From 4041bb10060e24c77631efbe1caf8b113fe21298 Mon Sep 17 00:00:00 2001 From: Matthias Geiger Date: Wed, 10 Jun 2026 21:31:57 +0200 Subject: [PATCH] Import solvespace_3.2+dfsg.orig.tar.xz [dgit import orig solvespace_3.2+dfsg.orig.tar.xz] --- .clang-format | 41 + .gitattributes | 10 + .github/ISSUE_TEMPLATE.md | 17 + .github/scripts/build-macos.sh | 51 + .github/scripts/build-snap.sh | 3 + .github/scripts/build-ubuntu.sh | 11 + .github/scripts/build-wasmlib.sh | 21 + .github/scripts/build-web.sh | 27 + .github/scripts/build-windows.sh | 55 + .github/scripts/install-macos.sh | 22 + .github/scripts/install-snap.sh | 6 + .github/scripts/install-ubuntu.sh | 10 + .github/scripts/install-web.sh | 3 + .github/scripts/install-windows.sh | 3 + .github/scripts/sign-macos.sh | 86 + .github/workflows/cd.yml | 225 + .github/workflows/python.yml | 181 + .github/workflows/source-tarball.yml | 56 + .github/workflows/test.yml | 61 + .github/workflows/wasmlib.yml | 79 + .gitignore | 26 + .gitmodules | 28 + .mailmap | 25 + CHANGELOG.md | 434 + CMakeLists.txt | 514 + CONTRIBUTING.md | 291 + COPYING.txt | 675 ++ README.md | 356 + THIRD_PARTIES.txt | 50 + bench/CMakeLists.txt | 17 + bench/harness.cpp | 85 + cmake/AddVendoredSubdirectory.cmake | 10 + cmake/DisableWarnings.cmake | 15 + cmake/FindCairo.cmake | 75 + cmake/FindSpaceWare.cmake | 28 + cmake/FindVendoredPackage.cmake | 56 + cmake/GetGitCommitHash.cmake | 43 + cmake/MacOSXBundleInfo.plist.in | 49 + cmake/Platform/Emscripten.cmake | 364 + cmake/Toolchain-mingw32.cmake | 15 + cmake/Toolchain-mingw64.cmake | 15 + cmake/c_flag_overrides.cmake | 11 + cmake/cxx_flag_overrides.cmake | 10 + developer_docs/IdLists_Entities_and_Remap.txt | 49 + developer_docs/Solver_Transforms.txt | 154 + exposed/CDemo.c | 302 + exposed/CMakeLists.txt | 12 + exposed/DOC.txt | 467 + exposed/VbDemo.vb | 1257 +++ include/slvs.h | 516 + js/README.md | 84 + js/slvs.d.ts | 194 + package.json | 21 + pkg/flatpak/.gitignore | 4 + pkg/flatpak/build.sh | 4 + pkg/flatpak/com.solvespace.SolveSpace.json | 267 + pkg/snap/.gitignore | 3 + pkg/snap/build.sh | 12 + pkg/snap/snap/snapcraft.yaml | 87 + pyproject.toml | 52 + python/slvs/__init__.py | 99 + python/slvs/solvespace.pyi | 280 + python/tests/__main__.py | 4 + python/tests/test.py | 253 + res/CMakeLists.txt | 326 + res/banner.txt | 1 + res/cocoa/AppIcon.iconset/icon_128x128.png | Bin 0 -> 891 bytes res/cocoa/AppIcon.iconset/icon_128x128@2x.png | Bin 0 -> 1303 bytes res/cocoa/AppIcon.iconset/icon_16x16.png | Bin 0 -> 586 bytes res/cocoa/AppIcon.iconset/icon_16x16@2x.png | Bin 0 -> 626 bytes res/cocoa/AppIcon.iconset/icon_256x256.png | Bin 0 -> 1303 bytes res/cocoa/AppIcon.iconset/icon_256x256@2x.png | Bin 0 -> 2506 bytes res/cocoa/AppIcon.iconset/icon_32x32.png | Bin 0 -> 626 bytes res/cocoa/AppIcon.iconset/icon_32x32@2x.png | Bin 0 -> 746 bytes res/cocoa/AppIcon.iconset/icon_512x512.png | Bin 0 -> 2506 bytes res/cocoa/AppIcon.iconset/icon_512x512@2x.png | Bin 0 -> 6490 bytes res/cocoa/MainMenu.xib | 65 + res/cocoa/SaveFormatAccessory.xib | 51 + res/fonts/BitstreamVeraSans-Roman-builtin.ttf | Bin 0 -> 54524 bytes res/fonts/private/0-check-false.png | Bin 0 -> 218 bytes res/fonts/private/1-check-true.png | Bin 0 -> 243 bytes res/fonts/private/2-radio-false.png | Bin 0 -> 228 bytes res/fonts/private/3-radio-true.png | Bin 0 -> 231 bytes res/fonts/private/4-stipple-dot.png | Bin 0 -> 936 bytes res/fonts/private/5-stipple-dash-long.png | Bin 0 -> 932 bytes res/fonts/private/6-stipple-dash.png | Bin 0 -> 932 bytes res/fonts/private/7-stipple-zigzag.png | Bin 0 -> 950 bytes res/fonts/unicode.lff.gz | Bin 0 -> 1081319 bytes .../com.solvespace.SolveSpace.metainfo.xml.in | 129 + res/freedesktop/solvespace-16x16.png | Bin 0 -> 336 bytes res/freedesktop/solvespace-24x24.png | Bin 0 -> 338 bytes res/freedesktop/solvespace-32x32.png | Bin 0 -> 348 bytes res/freedesktop/solvespace-48x48.png | Bin 0 -> 352 bytes res/freedesktop/solvespace-flatpak-mime.xml | 8 + res/freedesktop/solvespace-flatpak.desktop.in | 10 + res/freedesktop/solvespace-mime.xml | 8 + res/freedesktop/solvespace-scalable.svg | 12 + res/freedesktop/solvespace-snap.desktop | 10 + res/freedesktop/solvespace.desktop.in | 10 + res/freedesktop/solvespace.thumbnailer | 4 + res/icons/graphics-window/angle.png | Bin 0 -> 258 bytes res/icons/graphics-window/arc.png | Bin 0 -> 218 bytes res/icons/graphics-window/assemble.png | Bin 0 -> 214 bytes res/icons/graphics-window/bezier.png | Bin 0 -> 202 bytes res/icons/graphics-window/circle.png | Bin 0 -> 190 bytes res/icons/graphics-window/construction.png | Bin 0 -> 203 bytes res/icons/graphics-window/equal.png | Bin 0 -> 249 bytes res/icons/graphics-window/extrude.png | Bin 0 -> 216 bytes res/icons/graphics-window/helix.png | Bin 0 -> 243 bytes res/icons/graphics-window/horiz.png | Bin 0 -> 161 bytes res/icons/graphics-window/image.png | Bin 0 -> 227 bytes res/icons/graphics-window/in3d.png | Bin 0 -> 229 bytes res/icons/graphics-window/lathe.png | Bin 0 -> 260 bytes res/icons/graphics-window/length.png | Bin 0 -> 223 bytes res/icons/graphics-window/line.png | Bin 0 -> 153 bytes res/icons/graphics-window/ontoworkplane.png | Bin 0 -> 160 bytes res/icons/graphics-window/other-supp.png | Bin 0 -> 235 bytes res/icons/graphics-window/parallel.png | Bin 0 -> 165 bytes res/icons/graphics-window/perpendicular.png | Bin 0 -> 214 bytes res/icons/graphics-window/point.png | Bin 0 -> 132 bytes res/icons/graphics-window/pointonx.png | Bin 0 -> 267 bytes res/icons/graphics-window/rectangle.png | Bin 0 -> 159 bytes res/icons/graphics-window/ref.png | Bin 0 -> 178 bytes res/icons/graphics-window/revolve.png | Bin 0 -> 264 bytes .../graphics-window/same-orientation.png | Bin 0 -> 202 bytes res/icons/graphics-window/sketch-in-3d.png | Bin 0 -> 196 bytes res/icons/graphics-window/sketch-in-plane.png | Bin 0 -> 214 bytes res/icons/graphics-window/step-rotate.png | Bin 0 -> 207 bytes res/icons/graphics-window/step-translate.png | Bin 0 -> 171 bytes res/icons/graphics-window/symmetric.png | Bin 0 -> 149 bytes res/icons/graphics-window/tangent-arc.png | Bin 0 -> 232 bytes res/icons/graphics-window/text.png | Bin 0 -> 217 bytes res/icons/graphics-window/trim.png | Bin 0 -> 224 bytes res/icons/graphics-window/vert.png | Bin 0 -> 177 bytes res/icons/text-window/constraint-dimo.png | Bin 0 -> 194 bytes res/icons/text-window/constraint-wo.png | Bin 0 -> 284 bytes res/icons/text-window/constraint.png | Bin 0 -> 199 bytes res/icons/text-window/construction.png | Bin 0 -> 203 bytes res/icons/text-window/edges.png | Bin 0 -> 285 bytes res/icons/text-window/faces.png | Bin 0 -> 352 bytes res/icons/text-window/mesh.png | Bin 0 -> 335 bytes res/icons/text-window/normal.png | Bin 0 -> 177 bytes res/icons/text-window/occluded-invisible.png | Bin 0 -> 196 bytes res/icons/text-window/occluded-stippled.png | Bin 0 -> 218 bytes res/icons/text-window/occluded-visible.png | Bin 0 -> 208 bytes res/icons/text-window/outlines.png | Bin 0 -> 289 bytes res/icons/text-window/point.png | Bin 0 -> 132 bytes res/icons/text-window/shaded.png | Bin 0 -> 403 bytes res/icons/text-window/workplane.png | Bin 0 -> 193 bytes res/locales.txt | 12 + res/locales/cs_CZ.po | 2300 +++++ res/locales/de_DE.po | 2617 +++++ res/locales/en_US.po | 2518 +++++ res/locales/es_AR.po | 2319 +++++ res/locales/fr_FR.po | 2641 ++++++ res/locales/ja_JP.po | 2384 +++++ res/locales/ru_RU.po | 2642 ++++++ res/locales/tr_TR.po | 2330 +++++ res/locales/uk_UA.po | 2329 +++++ res/locales/zh_CN.po | 2383 +++++ res/messages.pot | 1980 ++++ res/qt-resources.qrc | 6 + res/shaders/edge.frag | 35 + res/shaders/edge.vert | 41 + res/shaders/imesh.frag | 12 + res/shaders/imesh.vert | 13 + res/shaders/imesh_point.frag | 15 + res/shaders/imesh_point.vert | 33 + res/shaders/imesh_tex.frag | 15 + res/shaders/imesh_tex.vert | 17 + res/shaders/imesh_texa.frag | 13 + res/shaders/mesh.frag | 26 + res/shaders/mesh.vert | 21 + res/shaders/mesh_fill.frag | 12 + res/shaders/mesh_fill.vert | 13 + res/shaders/outline.vert | 60 + res/threejs/SolveSpaceControls.js | 536 ++ res/threejs/hammer-2.0.8.js.gz | Bin 0 -> 17503 bytes res/win32/icon.ico | Bin 0 -> 29926 bytes res/win32/manifest.xml | 28 + res/win32/versioninfo.rc.in | 29 + src/CMakeLists.txt | 526 ++ src/bsp.cpp | 733 ++ src/clipboard.cpp | 527 ++ src/config.h.in | 19 + src/confscreen.cpp | 592 ++ src/constraint.cpp | 994 ++ src/constrainteq.cpp | 1062 +++ src/defs.h | 15 + src/describescreen.cpp | 589 ++ src/draw.cpp | 961 ++ src/drawconstraint.cpp | 1384 +++ src/drawentity.cpp | 867 ++ src/dsc.h | 730 ++ src/entity.cpp | 997 ++ src/export.cpp | 1272 +++ src/exportstep.cpp | 721 ++ src/exportvector.cpp | 1360 +++ src/expr.cpp | 963 ++ src/expr.h | 154 + src/file.cpp | 1030 ++ src/generate.cpp | 594 ++ src/graphicswin.cpp | 1481 +++ src/group.cpp | 1227 +++ src/groupmesh.cpp | 803 ++ src/handle.h | 47 + src/importdxf.cpp | 1199 +++ src/importidf.cpp | 536 ++ src/importmesh.cpp | 258 + src/mesh.cpp | 1221 +++ src/modify.cpp | 751 ++ src/mouse.cpp | 1599 ++++ src/param.h | 52 + src/platform/entrycli.cpp | 399 + src/platform/entrygui.cpp | 42 + src/platform/gui.cpp | 136 + src/platform/gui.h | 400 + src/platform/guigtk.cpp | 1679 ++++ src/platform/guihtml.cpp | 1449 +++ src/platform/guimac.mm | 1688 ++++ src/platform/guinone.cpp | 145 + src/platform/guiqt.cpp | 1076 +++ src/platform/guiqt.h | 132 + src/platform/guiwin.cpp | 1740 ++++ src/platform/html/emshell.html | 91 + src/platform/html/filemanagerui.js | 525 ++ src/platform/html/solvespaceui.css | 344 + src/platform/html/solvespaceui.js | 813 ++ src/platform/platform.cpp | 657 ++ src/platform/platform.h | 87 + src/platform/platformbase.cpp | 101 + src/polygon.cpp | 924 ++ src/polygon.h | 432 + src/polyline.cpp | 247 + src/render/gl3shader.cpp | 1071 +++ src/render/gl3shader.h | 271 + src/render/render.cpp | 459 + src/render/render.h | 408 + src/render/render2d.cpp | 413 + src/render/rendercairo.cpp | 173 + src/render/rendergl1.cpp | 857 ++ src/render/rendergl3.cpp | 1102 +++ src/request.cpp | 250 + src/resource.cpp | 1587 ++++ src/resource.h | 127 + src/sketch.h | 1002 ++ src/slvs/CMakeLists.txt | 80 + src/slvs/jslib.cpp | 210 + src/slvs/lib.cpp | 1124 +++ src/slvs/lib.pyx | 391 + src/solvespace.cpp | 1236 +++ src/solvespace.h | 749 ++ src/srf/boolean.cpp | 1049 ++ src/srf/curve.cpp | 886 ++ src/srf/merge.cpp | 130 + src/srf/ratpoly.cpp | 704 ++ src/srf/raycast.cpp | 616 ++ src/srf/shell.cpp | 618 ++ src/srf/surface.cpp | 494 + src/srf/surface.h | 449 + src/srf/surfinter.cpp | 596 ++ src/srf/triangulate.cpp | 738 ++ src/style.cpp | 948 ++ src/system.cpp | 614 ++ src/textscreens.cpp | 984 ++ src/textwin.cpp | 1173 +++ src/toolbar.cpp | 252 + src/ttf.cpp | 449 + src/ttf.h | 68 + src/ui.h | 889 ++ src/undoredo.cpp | 157 + src/util.cpp | 1081 +++ src/util.h | 105 + src/view.cpp | 174 + test/CMakeLists.txt | 141 + test/Gentium-R.ttf | Bin 0 -> 362644 bytes test/analysis/contour_area/normal.png | Bin 0 -> 4818 bytes test/analysis/contour_area/normal.slvs | 512 + test/analysis/contour_area/test.cpp | 9 + test/commit.sh | 9 + test/constraint/angle/free_in_3d.png | Bin 0 -> 4763 bytes test/constraint/angle/free_in_3d.slvs | 355 + test/constraint/angle/free_in_3d_v20.slvs | 355 + test/constraint/angle/free_in_3d_v22.slvs | 355 + test/constraint/angle/normal.png | Bin 0 -> 4882 bytes test/constraint/angle/normal.slvs | 352 + test/constraint/angle/normal_v20.slvs | 352 + test/constraint/angle/normal_v22.slvs | 352 + test/constraint/angle/reference.png | Bin 0 -> 5239 bytes test/constraint/angle/reference.slvs | 366 + .../constraint/angle/reference_free_in_3d.png | Bin 0 -> 4857 bytes .../angle/reference_free_in_3d.slvs | 355 + .../angle/reference_free_in_3d_v20.slvs | 355 + .../angle/reference_free_in_3d_v22.slvs | 355 + test/constraint/angle/reference_v20.slvs | 366 + test/constraint/angle/reference_v22.slvs | 366 + test/constraint/angle/skew.png | Bin 0 -> 4867 bytes test/constraint/angle/skew.slvs | 340 + test/constraint/angle/skew_v22.slvs | 340 + test/constraint/angle/test.cpp | 70 + test/constraint/arc_line_tangent/normal.png | Bin 0 -> 5100 bytes test/constraint/arc_line_tangent/normal.slvs | 450 + .../arc_line_tangent/normal_v20.slvs | 450 + .../arc_line_tangent/normal_v22.slvs | 450 + test/constraint/arc_line_tangent/test.cpp | 17 + .../at_midpoint/line_plane_free_in_3d.png | Bin 0 -> 4333 bytes .../at_midpoint/line_plane_free_in_3d.slvs | 290 + .../line_plane_free_in_3d_v20.slvs | 288 + .../line_plane_free_in_3d_v22.slvs | 290 + .../at_midpoint/line_plane_normal.png | Bin 0 -> 4333 bytes .../at_midpoint/line_plane_normal.slvs | 289 + .../at_midpoint/line_plane_normal_v20.slvs | 287 + .../at_midpoint/line_plane_normal_v22.slvs | 289 + .../at_midpoint/line_pt_free_in_3d.png | Bin 0 -> 4298 bytes .../at_midpoint/line_pt_free_in_3d.slvs | 315 + .../at_midpoint/line_pt_free_in_3d_v20.slvs | 313 + .../at_midpoint/line_pt_free_in_3d_v22.slvs | 315 + .../constraint/at_midpoint/line_pt_normal.png | Bin 0 -> 4298 bytes .../at_midpoint/line_pt_normal.slvs | 313 + .../at_midpoint/line_pt_normal_v20.slvs | 311 + .../at_midpoint/line_pt_normal_v22.slvs | 313 + test/constraint/at_midpoint/test.cpp | 65 + test/constraint/comment/normal.png | Bin 0 -> 4314 bytes test/constraint/comment/normal.slvs | 240 + test/constraint/comment/normal_v20.slvs | 238 + test/constraint/comment/normal_v22.slvs | 240 + test/constraint/comment/test.cpp | 17 + .../cubic_line_tangent/free_in_3d.png | Bin 0 -> 5073 bytes .../cubic_line_tangent/free_in_3d.slvs | 397 + .../cubic_line_tangent/free_in_3d_v20.slvs | 392 + .../cubic_line_tangent/free_in_3d_v22.slvs | 392 + test/constraint/cubic_line_tangent/normal.png | Bin 0 -> 5220 bytes .../constraint/cubic_line_tangent/normal.slvs | 446 + .../cubic_line_tangent/normal_v20.slvs | 446 + .../cubic_line_tangent/normal_v22.slvs | 446 + test/constraint/cubic_line_tangent/test.cpp | 33 + .../curve_curve_tangent/arc_arc.png | Bin 0 -> 5027 bytes .../curve_curve_tangent/arc_arc.slvs | 406 + .../curve_curve_tangent/arc_arc_v20.slvs | 404 + .../curve_curve_tangent/arc_arc_v22.slvs | 406 + .../curve_curve_tangent/arc_cubic.png | Bin 0 -> 5201 bytes .../curve_curve_tangent/arc_cubic.slvs | 506 + .../curve_curve_tangent/arc_cubic_v20.slvs | 506 + .../curve_curve_tangent/arc_cubic_v22.slvs | 506 + test/constraint/curve_curve_tangent/test.cpp | 33 + test/constraint/diameter/normal.png | Bin 0 -> 5024 bytes test/constraint/diameter/normal.slvs | 296 + test/constraint/diameter/normal_v20.slvs | 294 + test/constraint/diameter/normal_v22.slvs | 296 + test/constraint/diameter/reference.png | Bin 0 -> 5101 bytes test/constraint/diameter/reference.slvs | 296 + test/constraint/diameter/reference_v20.slvs | 296 + test/constraint/diameter/reference_v22.slvs | 296 + test/constraint/diameter/test.cpp | 33 + test/constraint/eq_len_pt_line_d/normal.png | Bin 0 -> 4407 bytes test/constraint/eq_len_pt_line_d/normal.slvs | 364 + .../eq_len_pt_line_d/normal_v20.slvs | 362 + .../eq_len_pt_line_d/normal_v22.slvs | 364 + test/constraint/eq_len_pt_line_d/test.cpp | 17 + test/constraint/eq_pt_ln_distances/normal.png | Bin 0 -> 4363 bytes .../constraint/eq_pt_ln_distances/normal.slvs | 389 + .../eq_pt_ln_distances/normal_v20.slvs | 387 + .../eq_pt_ln_distances/normal_v22.slvs | 389 + test/constraint/eq_pt_ln_distances/test.cpp | 17 + test/constraint/equal_angle/normal.png | Bin 0 -> 5271 bytes test/constraint/equal_angle/normal.slvs | 463 + .../equal_angle/normal_old_version.slvs | 463 + test/constraint/equal_angle/normal_v20.slvs | 461 + test/constraint/equal_angle/normal_v22.slvs | 463 + test/constraint/equal_angle/other.png | Bin 0 -> 4914 bytes test/constraint/equal_angle/other.slvs | 463 + test/constraint/equal_angle/other_v20.slvs | 463 + test/constraint/equal_angle/other_v22.slvs | 463 + test/constraint/equal_angle/test.cpp | 33 + test/constraint/equal_length_lines/normal.png | Bin 0 -> 4341 bytes .../constraint/equal_length_lines/normal.slvs | 339 + .../equal_length_lines/normal_v20.slvs | 337 + .../equal_length_lines/normal_v22.slvs | 339 + test/constraint/equal_length_lines/test.cpp | 17 + test/constraint/equal_line_arc_len/normal.png | Bin 0 -> 4564 bytes .../constraint/equal_line_arc_len/normal.slvs | 367 + .../equal_line_arc_len/normal_v20.slvs | 367 + .../equal_line_arc_len/normal_v22.slvs | 367 + test/constraint/equal_line_arc_len/pi.png | Bin 0 -> 4667 bytes test/constraint/equal_line_arc_len/pi.slvs | 367 + test/constraint/equal_line_arc_len/tau.png | Bin 0 -> 4963 bytes test/constraint/equal_line_arc_len/tau.slvs | 367 + test/constraint/equal_line_arc_len/test.cpp | 27 + test/constraint/equal_radius/normal.png | Bin 0 -> 5258 bytes test/constraint/equal_radius/normal.slvs | 349 + test/constraint/equal_radius/normal_v20.slvs | 347 + test/constraint/equal_radius/normal_v22.slvs | 349 + test/constraint/equal_radius/test.cpp | 17 + test/constraint/horizontal/line.png | Bin 0 -> 4274 bytes test/constraint/horizontal/line.slvs | 288 + test/constraint/horizontal/line_v20.slvs | 286 + test/constraint/horizontal/line_v22.slvs | 288 + test/constraint/horizontal/pt_pt.png | Bin 0 -> 4268 bytes test/constraint/horizontal/pt_pt.slvs | 287 + test/constraint/horizontal/pt_pt_v20.slvs | 287 + test/constraint/horizontal/pt_pt_v22.slvs | 287 + test/constraint/horizontal/test.cpp | 33 + test/constraint/length_difference/normal.png | Bin 0 -> 4462 bytes test/constraint/length_difference/normal.slvs | 341 + .../length_difference/normal_v22.slvs | 341 + .../length_difference/reference.png | Bin 0 -> 4542 bytes .../length_difference/reference.slvs | 341 + .../length_difference/reference_v22.slvs | 341 + test/constraint/length_difference/test.cpp | 23 + test/constraint/length_ratio/normal.png | Bin 0 -> 4546 bytes test/constraint/length_ratio/normal.slvs | 342 + test/constraint/length_ratio/normal_v20.slvs | 340 + test/constraint/length_ratio/normal_v22.slvs | 342 + test/constraint/length_ratio/reference.png | Bin 0 -> 4616 bytes test/constraint/length_ratio/reference.slvs | 342 + .../length_ratio/reference_v20.slvs | 342 + .../length_ratio/reference_v22.slvs | 342 + test/constraint/length_ratio/test.cpp | 33 + test/constraint/parallel/free_in_3d.png | Bin 0 -> 4304 bytes test/constraint/parallel/free_in_3d.slvs | 295 + test/constraint/parallel/free_in_3d_v20.slvs | 290 + test/constraint/parallel/free_in_3d_v22.slvs | 290 + test/constraint/parallel/normal.png | Bin 0 -> 4686 bytes test/constraint/parallel/normal.slvs | 339 + test/constraint/parallel/normal_v20.slvs | 337 + test/constraint/parallel/normal_v22.slvs | 339 + test/constraint/parallel/test.cpp | 33 + test/constraint/perpendicular/normal.png | Bin 0 -> 4910 bytes test/constraint/perpendicular/normal.slvs | 339 + test/constraint/perpendicular/normal_v20.slvs | 337 + test/constraint/perpendicular/normal_v22.slvs | 339 + test/constraint/perpendicular/test.cpp | 17 + .../points_coincident/free_in_3d.png | Bin 0 -> 4241 bytes .../points_coincident/free_in_3d.slvs | 288 + .../points_coincident/free_in_3d_v20.slvs | 288 + .../points_coincident/free_in_3d_v22.slvs | 288 + test/constraint/points_coincident/normal.png | Bin 0 -> 4241 bytes test/constraint/points_coincident/normal.slvs | 287 + .../points_coincident/normal_v20.slvs | 287 + .../points_coincident/normal_v22.slvs | 287 + test/constraint/points_coincident/test.cpp | 33 + test/constraint/proj_pt_distance/normal.png | Bin 0 -> 4518 bytes test/constraint/proj_pt_distance/normal.slvs | 290 + .../proj_pt_distance/normal_v20.slvs | 293 + .../proj_pt_distance/normal_v22.slvs | 290 + .../constraint/proj_pt_distance/reference.png | Bin 0 -> 4566 bytes .../proj_pt_distance/reference.slvs | 290 + .../proj_pt_distance/reference_v20.slvs | 292 + .../proj_pt_distance/reference_v22.slvs | 290 + test/constraint/proj_pt_distance/test.cpp | 33 + test/constraint/pt_face_distance/normal.png | Bin 0 -> 4314 bytes test/constraint/pt_face_distance/normal.slvs | 713 ++ .../pt_face_distance/normal_v20.slvs | 712 ++ .../pt_face_distance/normal_v22.slvs | 681 ++ .../constraint/pt_face_distance/reference.png | Bin 0 -> 4378 bytes .../pt_face_distance/reference.slvs | 713 ++ .../pt_face_distance/reference_v20.slvs | 712 ++ .../pt_face_distance/reference_v22.slvs | 681 ++ test/constraint/pt_face_distance/test.cpp | 33 + test/constraint/pt_in_plane/normal.png | Bin 0 -> 4228 bytes test/constraint/pt_in_plane/normal.slvs | 261 + test/constraint/pt_in_plane/normal_v20.slvs | 259 + test/constraint/pt_in_plane/normal_v22.slvs | 261 + test/constraint/pt_in_plane/test.cpp | 17 + test/constraint/pt_line_distance/extended.png | Bin 0 -> 4527 bytes .../constraint/pt_line_distance/extended.slvs | 316 + .../pt_line_distance/free_in_3d.png | Bin 0 -> 4403 bytes .../pt_line_distance/free_in_3d.slvs | 316 + .../pt_line_distance/free_in_3d_v20.slvs | 314 + .../pt_line_distance/free_in_3d_v22.slvs | 316 + test/constraint/pt_line_distance/normal.png | Bin 0 -> 4403 bytes test/constraint/pt_line_distance/normal.slvs | 314 + .../pt_line_distance/normal_v20.slvs | 314 + .../pt_line_distance/normal_v22.slvs | 314 + .../constraint/pt_line_distance/reference.png | Bin 0 -> 4475 bytes .../pt_line_distance/reference.slvs | 314 + .../pt_line_distance/reference_v20.slvs | 314 + .../pt_line_distance/reference_v22.slvs | 314 + test/constraint/pt_line_distance/test.cpp | 54 + .../constraint/pt_on_circle/negative_dia.slvs | 364 + test/constraint/pt_on_circle/normal.png | Bin 0 -> 4884 bytes test/constraint/pt_on_circle/normal.slvs | 318 + test/constraint/pt_on_circle/normal_v20.slvs | 318 + test/constraint/pt_on_circle/normal_v22.slvs | 318 + test/constraint/pt_on_circle/test.cpp | 25 + test/constraint/pt_on_face/normal.png | Bin 0 -> 4662 bytes test/constraint/pt_on_face/normal.slvs | 703 ++ test/constraint/pt_on_face/normal_v20.slvs | 670 ++ test/constraint/pt_on_face/normal_v22.slvs | 687 ++ test/constraint/pt_on_face/test.cpp | 17 + .../constraint/pt_on_line/free_in_3d_v20.slvs | 315 + .../constraint/pt_on_line/free_in_3d_v22.slvs | 315 + .../constraint/pt_on_line/left_free_in_3d.png | Bin 0 -> 4319 bytes .../pt_on_line/left_free_in_3d.slvs | 320 + test/constraint/pt_on_line/normal.png | Bin 0 -> 4324 bytes test/constraint/pt_on_line/normal.slvs | 318 + test/constraint/pt_on_line/normal_v20.slvs | 315 + test/constraint/pt_on_line/normal_v22.slvs | 313 + .../pt_on_line/right_free_in_3d.png | Bin 0 -> 4316 bytes .../pt_on_line/right_free_in_3d.slvs | 320 + test/constraint/pt_on_line/test.cpp | 39 + test/constraint/pt_plane_distance/normal.png | Bin 0 -> 4345 bytes test/constraint/pt_plane_distance/normal.slvs | 264 + .../pt_plane_distance/normal_v20.slvs | 262 + .../pt_plane_distance/normal_v22.slvs | 264 + .../pt_plane_distance/reference.png | Bin 0 -> 4417 bytes .../pt_plane_distance/reference.slvs | 264 + .../pt_plane_distance/reference_v20.slvs | 264 + .../pt_plane_distance/reference_v22.slvs | 264 + test/constraint/pt_plane_distance/test.cpp | 33 + test/constraint/pt_pt_distance/free_in_3d.png | Bin 0 -> 4462 bytes .../constraint/pt_pt_distance/free_in_3d.slvs | 289 + .../pt_pt_distance/free_in_3d_v20.slvs | 289 + .../pt_pt_distance/free_in_3d_v22.slvs | 289 + test/constraint/pt_pt_distance/normal.png | Bin 0 -> 4467 bytes test/constraint/pt_pt_distance/normal.slvs | 289 + .../constraint/pt_pt_distance/normal_v20.slvs | 287 + .../constraint/pt_pt_distance/normal_v22.slvs | 289 + test/constraint/pt_pt_distance/reference.png | Bin 0 -> 4539 bytes test/constraint/pt_pt_distance/reference.slvs | 289 + .../pt_pt_distance/reference_v20.slvs | 289 + .../pt_pt_distance/reference_v22.slvs | 289 + test/constraint/pt_pt_distance/test.cpp | 49 + test/constraint/same_orientation/normal.png | Bin 0 -> 4472 bytes test/constraint/same_orientation/normal.slvs | 309 + .../same_orientation/normal_v20.slvs | 302 + .../same_orientation/normal_v22.slvs | 304 + .../same_orientation/same_group.png | Bin 0 -> 4376 bytes .../same_orientation/same_group.slvs | 379 + test/constraint/same_orientation/test.cpp | 23 + test/constraint/symmetric/free_in_3d.png | Bin 0 -> 4378 bytes test/constraint/symmetric/free_in_3d.slvs | 289 + test/constraint/symmetric/free_in_3d_v20.slvs | 287 + test/constraint/symmetric/free_in_3d_v22.slvs | 289 + test/constraint/symmetric/normal.png | Bin 0 -> 4362 bytes test/constraint/symmetric/normal.slvs | 288 + test/constraint/symmetric/normal_v20.slvs | 288 + test/constraint/symmetric/normal_v22.slvs | 288 + test/constraint/symmetric/test.cpp | 33 + test/constraint/symmetric_horiz/normal.png | Bin 0 -> 4362 bytes test/constraint/symmetric_horiz/normal.slvs | 287 + .../symmetric_horiz/normal_v20.slvs | 285 + .../symmetric_horiz/normal_v22.slvs | 287 + test/constraint/symmetric_horiz/test.cpp | 17 + test/constraint/symmetric_line/normal.png | Bin 0 -> 4431 bytes test/constraint/symmetric_line/normal.slvs | 338 + .../constraint/symmetric_line/normal_v20.slvs | 336 + .../constraint/symmetric_line/normal_v22.slvs | 338 + test/constraint/symmetric_line/test.cpp | 17 + test/constraint/symmetric_vert/normal.png | Bin 0 -> 4378 bytes test/constraint/symmetric_vert/normal.slvs | 287 + .../constraint/symmetric_vert/normal_v20.slvs | 285 + .../constraint/symmetric_vert/normal_v22.slvs | 287 + test/constraint/symmetric_vert/test.cpp | 17 + test/constraint/vertical/line.png | Bin 0 -> 4331 bytes test/constraint/vertical/line.slvs | 288 + test/constraint/vertical/line_v20.slvs | 290 + test/constraint/vertical/line_v22.slvs | 288 + test/constraint/vertical/pt_pt.png | Bin 0 -> 4258 bytes test/constraint/vertical/pt_pt.slvs | 287 + test/constraint/vertical/pt_pt_v20.slvs | 285 + test/constraint/vertical/pt_pt_v22.slvs | 287 + test/constraint/vertical/test.cpp | 33 + test/constraint/where_dragged/free_in_3d.png | Bin 0 -> 4257 bytes test/constraint/where_dragged/free_in_3d.slvs | 261 + .../where_dragged/free_in_3d_v20.slvs | 261 + .../where_dragged/free_in_3d_v22.slvs | 261 + test/constraint/where_dragged/normal.png | Bin 0 -> 4257 bytes test/constraint/where_dragged/normal.slvs | 262 + test/constraint/where_dragged/normal_v20.slvs | 260 + test/constraint/where_dragged/normal_v22.slvs | 262 + test/constraint/where_dragged/test.cpp | 33 + test/core/expr/test.cpp | 114 + test/core/locale/test.cpp | 10 + test/core/path/test.cpp | 248 + test/debugtool.cpp | 35 + test/group/link/normal.png | Bin 0 -> 4151 bytes test/group/link/normal.slvs | 465 + test/group/link/normal_v20.slvs | 463 + test/group/link/normal_v22.slvs | 465 + test/group/link/rect_v20.slvs | 510 + test/group/link/test.cpp | 17 + test/group/translate_asy/normal.png | Bin 0 -> 8508 bytes test/group/translate_asy/normal.slvs | 2104 +++++ test/group/translate_asy/normal_v22.slvs | 2088 ++++ test/group/translate_asy/test.cpp | 31 + test/group/translate_nd/normal.png | Bin 0 -> 4983 bytes test/group/translate_nd/normal.slvs | 8396 +++++++++++++++++ test/group/translate_nd/normal_v22.slvs | 8316 ++++++++++++++++ test/group/translate_nd/test.cpp | 12 + test/harness.cpp | 429 + test/harness.h | 78 + test/request/arc_of_circle/normal.png | Bin 0 -> 4560 bytes test/request/arc_of_circle/normal.slvs | 306 + test/request/arc_of_circle/normal_v20.slvs | 306 + test/request/arc_of_circle/normal_v22.slvs | 306 + test/request/arc_of_circle/test.cpp | 17 + test/request/circle/free_in_3d.png | Bin 0 -> 4796 bytes test/request/circle/free_in_3d.slvs | 294 + test/request/circle/free_in_3d_dof.png | Bin 0 -> 7195 bytes test/request/circle/free_in_3d_v20.slvs | 292 + test/request/circle/free_in_3d_v22.slvs | 294 + test/request/circle/normal.png | Bin 0 -> 4796 bytes test/request/circle/normal.slvs | 283 + test/request/circle/normal_dof.png | Bin 0 -> 4830 bytes test/request/circle/normal_v20.slvs | 283 + test/request/circle/normal_v22.slvs | 283 + test/request/circle/test.cpp | 47 + test/request/cubic/normal.png | Bin 0 -> 4949 bytes test/request/cubic/normal.slvs | 348 + test/request/cubic/normal_v20.slvs | 348 + test/request/cubic/normal_v22.slvs | 348 + test/request/cubic/test.cpp | 17 + test/request/cubic_periodic/normal.png | Bin 0 -> 5225 bytes test/request/cubic_periodic/normal.slvs | 316 + test/request/cubic_periodic/normal_v20.slvs | 314 + test/request/cubic_periodic/normal_v22.slvs | 316 + test/request/cubic_periodic/test.cpp | 17 + test/request/datum_point/normal.png | Bin 0 -> 4205 bytes test/request/datum_point/normal.slvs | 252 + test/request/datum_point/normal_v20.slvs | 250 + test/request/datum_point/normal_v22.slvs | 252 + test/request/datum_point/test.cpp | 17 + test/request/image/drawing.png | Bin 0 -> 8565 bytes test/request/image/linked.png | Bin 0 -> 4309 bytes test/request/image/linked.slvs | 430 + test/request/image/normal.png | Bin 0 -> 4292 bytes test/request/image/normal.slvs | 319 + test/request/image/test.cpp | 13 + test/request/line_segment/normal.png | Bin 0 -> 4246 bytes test/request/line_segment/normal.slvs | 278 + test/request/line_segment/normal_v20.slvs | 278 + test/request/line_segment/normal_v22.slvs | 278 + test/request/line_segment/test.cpp | 17 + test/request/ttf_text/kerning.png | Bin 0 -> 6122 bytes test/request/ttf_text/kerning.slvs | 331 + test/request/ttf_text/normal.png | Bin 0 -> 6158 bytes test/request/ttf_text/normal.slvs | 329 + test/request/ttf_text/normal_v20.slvs | 290 + test/request/ttf_text/normal_v22.slvs | 292 + test/request/ttf_text/test.cpp | 23 + test/request/workplane/normal.png | Bin 0 -> 5612 bytes test/request/workplane/normal.slvs | 293 + test/request/workplane/normal_v20.slvs | 291 + test/request/workplane/normal_v22.slvs | 293 + test/request/workplane/test.cpp | 17 + wishlist.txt | 19 + 647 files changed, 199672 insertions(+) create mode 100644 .clang-format create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100755 .github/scripts/build-macos.sh create mode 100755 .github/scripts/build-snap.sh create mode 100755 .github/scripts/build-ubuntu.sh create mode 100755 .github/scripts/build-wasmlib.sh create mode 100755 .github/scripts/build-web.sh create mode 100755 .github/scripts/build-windows.sh create mode 100755 .github/scripts/install-macos.sh create mode 100755 .github/scripts/install-snap.sh create mode 100755 .github/scripts/install-ubuntu.sh create mode 100755 .github/scripts/install-web.sh create mode 100755 .github/scripts/install-windows.sh create mode 100755 .github/scripts/sign-macos.sh create mode 100644 .github/workflows/cd.yml create mode 100644 .github/workflows/python.yml create mode 100644 .github/workflows/source-tarball.yml create mode 100644 .github/workflows/test.yml create mode 100644 .github/workflows/wasmlib.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .mailmap create mode 100644 CHANGELOG.md create mode 100644 CMakeLists.txt create mode 100644 CONTRIBUTING.md create mode 100644 COPYING.txt create mode 100644 README.md create mode 100644 THIRD_PARTIES.txt create mode 100644 bench/CMakeLists.txt create mode 100644 bench/harness.cpp create mode 100644 cmake/AddVendoredSubdirectory.cmake create mode 100644 cmake/DisableWarnings.cmake create mode 100644 cmake/FindCairo.cmake create mode 100644 cmake/FindSpaceWare.cmake create mode 100644 cmake/FindVendoredPackage.cmake create mode 100644 cmake/GetGitCommitHash.cmake create mode 100644 cmake/MacOSXBundleInfo.plist.in create mode 100644 cmake/Platform/Emscripten.cmake create mode 100644 cmake/Toolchain-mingw32.cmake create mode 100644 cmake/Toolchain-mingw64.cmake create mode 100644 cmake/c_flag_overrides.cmake create mode 100644 cmake/cxx_flag_overrides.cmake create mode 100644 developer_docs/IdLists_Entities_and_Remap.txt create mode 100644 developer_docs/Solver_Transforms.txt create mode 100644 exposed/CDemo.c create mode 100644 exposed/CMakeLists.txt create mode 100644 exposed/DOC.txt create mode 100644 exposed/VbDemo.vb create mode 100644 include/slvs.h create mode 100644 js/README.md create mode 100644 js/slvs.d.ts create mode 100644 package.json create mode 100644 pkg/flatpak/.gitignore create mode 100755 pkg/flatpak/build.sh create mode 100644 pkg/flatpak/com.solvespace.SolveSpace.json create mode 100644 pkg/snap/.gitignore create mode 100755 pkg/snap/build.sh create mode 100644 pkg/snap/snap/snapcraft.yaml create mode 100644 pyproject.toml create mode 100644 python/slvs/__init__.py create mode 100644 python/slvs/solvespace.pyi create mode 100644 python/tests/__main__.py create mode 100644 python/tests/test.py create mode 100644 res/CMakeLists.txt create mode 100644 res/banner.txt create mode 100644 res/cocoa/AppIcon.iconset/icon_128x128.png create mode 100644 res/cocoa/AppIcon.iconset/icon_128x128@2x.png create mode 100644 res/cocoa/AppIcon.iconset/icon_16x16.png create mode 100644 res/cocoa/AppIcon.iconset/icon_16x16@2x.png create mode 100644 res/cocoa/AppIcon.iconset/icon_256x256.png create mode 100644 res/cocoa/AppIcon.iconset/icon_256x256@2x.png create mode 100644 res/cocoa/AppIcon.iconset/icon_32x32.png create mode 100644 res/cocoa/AppIcon.iconset/icon_32x32@2x.png create mode 100644 res/cocoa/AppIcon.iconset/icon_512x512.png create mode 100644 res/cocoa/AppIcon.iconset/icon_512x512@2x.png create mode 100644 res/cocoa/MainMenu.xib create mode 100644 res/cocoa/SaveFormatAccessory.xib create mode 100644 res/fonts/BitstreamVeraSans-Roman-builtin.ttf create mode 100644 res/fonts/private/0-check-false.png create mode 100644 res/fonts/private/1-check-true.png create mode 100644 res/fonts/private/2-radio-false.png create mode 100644 res/fonts/private/3-radio-true.png create mode 100644 res/fonts/private/4-stipple-dot.png create mode 100644 res/fonts/private/5-stipple-dash-long.png create mode 100644 res/fonts/private/6-stipple-dash.png create mode 100644 res/fonts/private/7-stipple-zigzag.png create mode 100644 res/fonts/unicode.lff.gz create mode 100644 res/freedesktop/com.solvespace.SolveSpace.metainfo.xml.in create mode 100644 res/freedesktop/solvespace-16x16.png create mode 100644 res/freedesktop/solvespace-24x24.png create mode 100644 res/freedesktop/solvespace-32x32.png create mode 100644 res/freedesktop/solvespace-48x48.png create mode 100644 res/freedesktop/solvespace-flatpak-mime.xml create mode 100644 res/freedesktop/solvespace-flatpak.desktop.in create mode 100644 res/freedesktop/solvespace-mime.xml create mode 100644 res/freedesktop/solvespace-scalable.svg create mode 100644 res/freedesktop/solvespace-snap.desktop create mode 100644 res/freedesktop/solvespace.desktop.in create mode 100644 res/freedesktop/solvespace.thumbnailer create mode 100644 res/icons/graphics-window/angle.png create mode 100644 res/icons/graphics-window/arc.png create mode 100644 res/icons/graphics-window/assemble.png create mode 100644 res/icons/graphics-window/bezier.png create mode 100644 res/icons/graphics-window/circle.png create mode 100644 res/icons/graphics-window/construction.png create mode 100644 res/icons/graphics-window/equal.png create mode 100644 res/icons/graphics-window/extrude.png create mode 100644 res/icons/graphics-window/helix.png create mode 100644 res/icons/graphics-window/horiz.png create mode 100644 res/icons/graphics-window/image.png create mode 100644 res/icons/graphics-window/in3d.png create mode 100644 res/icons/graphics-window/lathe.png create mode 100644 res/icons/graphics-window/length.png create mode 100644 res/icons/graphics-window/line.png create mode 100644 res/icons/graphics-window/ontoworkplane.png create mode 100644 res/icons/graphics-window/other-supp.png create mode 100644 res/icons/graphics-window/parallel.png create mode 100644 res/icons/graphics-window/perpendicular.png create mode 100644 res/icons/graphics-window/point.png create mode 100644 res/icons/graphics-window/pointonx.png create mode 100644 res/icons/graphics-window/rectangle.png create mode 100644 res/icons/graphics-window/ref.png create mode 100644 res/icons/graphics-window/revolve.png create mode 100644 res/icons/graphics-window/same-orientation.png create mode 100644 res/icons/graphics-window/sketch-in-3d.png create mode 100644 res/icons/graphics-window/sketch-in-plane.png create mode 100644 res/icons/graphics-window/step-rotate.png create mode 100644 res/icons/graphics-window/step-translate.png create mode 100644 res/icons/graphics-window/symmetric.png create mode 100644 res/icons/graphics-window/tangent-arc.png create mode 100644 res/icons/graphics-window/text.png create mode 100644 res/icons/graphics-window/trim.png create mode 100644 res/icons/graphics-window/vert.png create mode 100644 res/icons/text-window/constraint-dimo.png create mode 100644 res/icons/text-window/constraint-wo.png create mode 100644 res/icons/text-window/constraint.png create mode 100644 res/icons/text-window/construction.png create mode 100644 res/icons/text-window/edges.png create mode 100644 res/icons/text-window/faces.png create mode 100644 res/icons/text-window/mesh.png create mode 100644 res/icons/text-window/normal.png create mode 100644 res/icons/text-window/occluded-invisible.png create mode 100644 res/icons/text-window/occluded-stippled.png create mode 100644 res/icons/text-window/occluded-visible.png create mode 100644 res/icons/text-window/outlines.png create mode 100644 res/icons/text-window/point.png create mode 100644 res/icons/text-window/shaded.png create mode 100644 res/icons/text-window/workplane.png create mode 100644 res/locales.txt create mode 100644 res/locales/cs_CZ.po create mode 100644 res/locales/de_DE.po create mode 100644 res/locales/en_US.po create mode 100644 res/locales/es_AR.po create mode 100644 res/locales/fr_FR.po create mode 100644 res/locales/ja_JP.po create mode 100644 res/locales/ru_RU.po create mode 100644 res/locales/tr_TR.po create mode 100644 res/locales/uk_UA.po create mode 100644 res/locales/zh_CN.po create mode 100644 res/messages.pot create mode 100644 res/qt-resources.qrc create mode 100644 res/shaders/edge.frag create mode 100644 res/shaders/edge.vert create mode 100644 res/shaders/imesh.frag create mode 100644 res/shaders/imesh.vert create mode 100644 res/shaders/imesh_point.frag create mode 100644 res/shaders/imesh_point.vert create mode 100644 res/shaders/imesh_tex.frag create mode 100644 res/shaders/imesh_tex.vert create mode 100644 res/shaders/imesh_texa.frag create mode 100644 res/shaders/mesh.frag create mode 100644 res/shaders/mesh.vert create mode 100644 res/shaders/mesh_fill.frag create mode 100644 res/shaders/mesh_fill.vert create mode 100644 res/shaders/outline.vert create mode 100644 res/threejs/SolveSpaceControls.js create mode 100644 res/threejs/hammer-2.0.8.js.gz create mode 100644 res/win32/icon.ico create mode 100644 res/win32/manifest.xml create mode 100644 res/win32/versioninfo.rc.in create mode 100644 src/CMakeLists.txt create mode 100644 src/bsp.cpp create mode 100644 src/clipboard.cpp create mode 100644 src/config.h.in create mode 100644 src/confscreen.cpp create mode 100644 src/constraint.cpp create mode 100644 src/constrainteq.cpp create mode 100644 src/defs.h create mode 100644 src/describescreen.cpp create mode 100644 src/draw.cpp create mode 100644 src/drawconstraint.cpp create mode 100644 src/drawentity.cpp create mode 100644 src/dsc.h create mode 100644 src/entity.cpp create mode 100644 src/export.cpp create mode 100644 src/exportstep.cpp create mode 100644 src/exportvector.cpp create mode 100644 src/expr.cpp create mode 100644 src/expr.h create mode 100644 src/file.cpp create mode 100644 src/generate.cpp create mode 100644 src/graphicswin.cpp create mode 100644 src/group.cpp create mode 100644 src/groupmesh.cpp create mode 100644 src/handle.h create mode 100644 src/importdxf.cpp create mode 100644 src/importidf.cpp create mode 100644 src/importmesh.cpp create mode 100644 src/mesh.cpp create mode 100644 src/modify.cpp create mode 100644 src/mouse.cpp create mode 100644 src/param.h create mode 100644 src/platform/entrycli.cpp create mode 100644 src/platform/entrygui.cpp create mode 100644 src/platform/gui.cpp create mode 100644 src/platform/gui.h create mode 100644 src/platform/guigtk.cpp create mode 100644 src/platform/guihtml.cpp create mode 100644 src/platform/guimac.mm create mode 100644 src/platform/guinone.cpp create mode 100644 src/platform/guiqt.cpp create mode 100644 src/platform/guiqt.h create mode 100644 src/platform/guiwin.cpp create mode 100644 src/platform/html/emshell.html create mode 100644 src/platform/html/filemanagerui.js create mode 100644 src/platform/html/solvespaceui.css create mode 100644 src/platform/html/solvespaceui.js create mode 100644 src/platform/platform.cpp create mode 100644 src/platform/platform.h create mode 100644 src/platform/platformbase.cpp create mode 100644 src/polygon.cpp create mode 100644 src/polygon.h create mode 100644 src/polyline.cpp create mode 100644 src/render/gl3shader.cpp create mode 100644 src/render/gl3shader.h create mode 100644 src/render/render.cpp create mode 100644 src/render/render.h create mode 100644 src/render/render2d.cpp create mode 100644 src/render/rendercairo.cpp create mode 100644 src/render/rendergl1.cpp create mode 100644 src/render/rendergl3.cpp create mode 100644 src/request.cpp create mode 100644 src/resource.cpp create mode 100644 src/resource.h create mode 100644 src/sketch.h create mode 100644 src/slvs/CMakeLists.txt create mode 100644 src/slvs/jslib.cpp create mode 100644 src/slvs/lib.cpp create mode 100644 src/slvs/lib.pyx create mode 100644 src/solvespace.cpp create mode 100644 src/solvespace.h create mode 100644 src/srf/boolean.cpp create mode 100644 src/srf/curve.cpp create mode 100644 src/srf/merge.cpp create mode 100644 src/srf/ratpoly.cpp create mode 100644 src/srf/raycast.cpp create mode 100644 src/srf/shell.cpp create mode 100644 src/srf/surface.cpp create mode 100644 src/srf/surface.h create mode 100644 src/srf/surfinter.cpp create mode 100644 src/srf/triangulate.cpp create mode 100644 src/style.cpp create mode 100644 src/system.cpp create mode 100644 src/textscreens.cpp create mode 100644 src/textwin.cpp create mode 100644 src/toolbar.cpp create mode 100644 src/ttf.cpp create mode 100644 src/ttf.h create mode 100644 src/ui.h create mode 100644 src/undoredo.cpp create mode 100644 src/util.cpp create mode 100644 src/util.h create mode 100644 src/view.cpp create mode 100644 test/CMakeLists.txt create mode 100644 test/Gentium-R.ttf create mode 100644 test/analysis/contour_area/normal.png create mode 100644 test/analysis/contour_area/normal.slvs create mode 100644 test/analysis/contour_area/test.cpp create mode 100755 test/commit.sh create mode 100644 test/constraint/angle/free_in_3d.png create mode 100644 test/constraint/angle/free_in_3d.slvs create mode 100644 test/constraint/angle/free_in_3d_v20.slvs create mode 100644 test/constraint/angle/free_in_3d_v22.slvs create mode 100644 test/constraint/angle/normal.png create mode 100644 test/constraint/angle/normal.slvs create mode 100644 test/constraint/angle/normal_v20.slvs create mode 100644 test/constraint/angle/normal_v22.slvs create mode 100644 test/constraint/angle/reference.png create mode 100644 test/constraint/angle/reference.slvs create mode 100644 test/constraint/angle/reference_free_in_3d.png create mode 100644 test/constraint/angle/reference_free_in_3d.slvs create mode 100644 test/constraint/angle/reference_free_in_3d_v20.slvs create mode 100644 test/constraint/angle/reference_free_in_3d_v22.slvs create mode 100644 test/constraint/angle/reference_v20.slvs create mode 100644 test/constraint/angle/reference_v22.slvs create mode 100644 test/constraint/angle/skew.png create mode 100644 test/constraint/angle/skew.slvs create mode 100644 test/constraint/angle/skew_v22.slvs create mode 100644 test/constraint/angle/test.cpp create mode 100644 test/constraint/arc_line_tangent/normal.png create mode 100644 test/constraint/arc_line_tangent/normal.slvs create mode 100644 test/constraint/arc_line_tangent/normal_v20.slvs create mode 100644 test/constraint/arc_line_tangent/normal_v22.slvs create mode 100644 test/constraint/arc_line_tangent/test.cpp create mode 100644 test/constraint/at_midpoint/line_plane_free_in_3d.png create mode 100644 test/constraint/at_midpoint/line_plane_free_in_3d.slvs create mode 100644 test/constraint/at_midpoint/line_plane_free_in_3d_v20.slvs create mode 100644 test/constraint/at_midpoint/line_plane_free_in_3d_v22.slvs create mode 100644 test/constraint/at_midpoint/line_plane_normal.png create mode 100644 test/constraint/at_midpoint/line_plane_normal.slvs create mode 100644 test/constraint/at_midpoint/line_plane_normal_v20.slvs create mode 100644 test/constraint/at_midpoint/line_plane_normal_v22.slvs create mode 100644 test/constraint/at_midpoint/line_pt_free_in_3d.png create mode 100644 test/constraint/at_midpoint/line_pt_free_in_3d.slvs create mode 100644 test/constraint/at_midpoint/line_pt_free_in_3d_v20.slvs create mode 100644 test/constraint/at_midpoint/line_pt_free_in_3d_v22.slvs create mode 100644 test/constraint/at_midpoint/line_pt_normal.png create mode 100644 test/constraint/at_midpoint/line_pt_normal.slvs create mode 100644 test/constraint/at_midpoint/line_pt_normal_v20.slvs create mode 100644 test/constraint/at_midpoint/line_pt_normal_v22.slvs create mode 100644 test/constraint/at_midpoint/test.cpp create mode 100644 test/constraint/comment/normal.png create mode 100644 test/constraint/comment/normal.slvs create mode 100644 test/constraint/comment/normal_v20.slvs create mode 100644 test/constraint/comment/normal_v22.slvs create mode 100644 test/constraint/comment/test.cpp create mode 100644 test/constraint/cubic_line_tangent/free_in_3d.png create mode 100644 test/constraint/cubic_line_tangent/free_in_3d.slvs create mode 100644 test/constraint/cubic_line_tangent/free_in_3d_v20.slvs create mode 100644 test/constraint/cubic_line_tangent/free_in_3d_v22.slvs create mode 100644 test/constraint/cubic_line_tangent/normal.png create mode 100644 test/constraint/cubic_line_tangent/normal.slvs create mode 100644 test/constraint/cubic_line_tangent/normal_v20.slvs create mode 100644 test/constraint/cubic_line_tangent/normal_v22.slvs create mode 100644 test/constraint/cubic_line_tangent/test.cpp create mode 100644 test/constraint/curve_curve_tangent/arc_arc.png create mode 100644 test/constraint/curve_curve_tangent/arc_arc.slvs create mode 100644 test/constraint/curve_curve_tangent/arc_arc_v20.slvs create mode 100644 test/constraint/curve_curve_tangent/arc_arc_v22.slvs create mode 100644 test/constraint/curve_curve_tangent/arc_cubic.png create mode 100644 test/constraint/curve_curve_tangent/arc_cubic.slvs create mode 100644 test/constraint/curve_curve_tangent/arc_cubic_v20.slvs create mode 100644 test/constraint/curve_curve_tangent/arc_cubic_v22.slvs create mode 100644 test/constraint/curve_curve_tangent/test.cpp create mode 100644 test/constraint/diameter/normal.png create mode 100644 test/constraint/diameter/normal.slvs create mode 100644 test/constraint/diameter/normal_v20.slvs create mode 100644 test/constraint/diameter/normal_v22.slvs create mode 100644 test/constraint/diameter/reference.png create mode 100644 test/constraint/diameter/reference.slvs create mode 100644 test/constraint/diameter/reference_v20.slvs create mode 100644 test/constraint/diameter/reference_v22.slvs create mode 100644 test/constraint/diameter/test.cpp create mode 100644 test/constraint/eq_len_pt_line_d/normal.png create mode 100644 test/constraint/eq_len_pt_line_d/normal.slvs create mode 100644 test/constraint/eq_len_pt_line_d/normal_v20.slvs create mode 100644 test/constraint/eq_len_pt_line_d/normal_v22.slvs create mode 100644 test/constraint/eq_len_pt_line_d/test.cpp create mode 100644 test/constraint/eq_pt_ln_distances/normal.png create mode 100644 test/constraint/eq_pt_ln_distances/normal.slvs create mode 100644 test/constraint/eq_pt_ln_distances/normal_v20.slvs create mode 100644 test/constraint/eq_pt_ln_distances/normal_v22.slvs create mode 100644 test/constraint/eq_pt_ln_distances/test.cpp create mode 100644 test/constraint/equal_angle/normal.png create mode 100644 test/constraint/equal_angle/normal.slvs create mode 100644 test/constraint/equal_angle/normal_old_version.slvs create mode 100644 test/constraint/equal_angle/normal_v20.slvs create mode 100644 test/constraint/equal_angle/normal_v22.slvs create mode 100644 test/constraint/equal_angle/other.png create mode 100644 test/constraint/equal_angle/other.slvs create mode 100644 test/constraint/equal_angle/other_v20.slvs create mode 100644 test/constraint/equal_angle/other_v22.slvs create mode 100644 test/constraint/equal_angle/test.cpp create mode 100644 test/constraint/equal_length_lines/normal.png create mode 100644 test/constraint/equal_length_lines/normal.slvs create mode 100644 test/constraint/equal_length_lines/normal_v20.slvs create mode 100644 test/constraint/equal_length_lines/normal_v22.slvs create mode 100644 test/constraint/equal_length_lines/test.cpp create mode 100644 test/constraint/equal_line_arc_len/normal.png create mode 100644 test/constraint/equal_line_arc_len/normal.slvs create mode 100644 test/constraint/equal_line_arc_len/normal_v20.slvs create mode 100644 test/constraint/equal_line_arc_len/normal_v22.slvs create mode 100644 test/constraint/equal_line_arc_len/pi.png create mode 100644 test/constraint/equal_line_arc_len/pi.slvs create mode 100644 test/constraint/equal_line_arc_len/tau.png create mode 100644 test/constraint/equal_line_arc_len/tau.slvs create mode 100644 test/constraint/equal_line_arc_len/test.cpp create mode 100644 test/constraint/equal_radius/normal.png create mode 100644 test/constraint/equal_radius/normal.slvs create mode 100644 test/constraint/equal_radius/normal_v20.slvs create mode 100644 test/constraint/equal_radius/normal_v22.slvs create mode 100644 test/constraint/equal_radius/test.cpp create mode 100644 test/constraint/horizontal/line.png create mode 100644 test/constraint/horizontal/line.slvs create mode 100644 test/constraint/horizontal/line_v20.slvs create mode 100644 test/constraint/horizontal/line_v22.slvs create mode 100644 test/constraint/horizontal/pt_pt.png create mode 100644 test/constraint/horizontal/pt_pt.slvs create mode 100644 test/constraint/horizontal/pt_pt_v20.slvs create mode 100644 test/constraint/horizontal/pt_pt_v22.slvs create mode 100644 test/constraint/horizontal/test.cpp create mode 100644 test/constraint/length_difference/normal.png create mode 100644 test/constraint/length_difference/normal.slvs create mode 100644 test/constraint/length_difference/normal_v22.slvs create mode 100644 test/constraint/length_difference/reference.png create mode 100644 test/constraint/length_difference/reference.slvs create mode 100644 test/constraint/length_difference/reference_v22.slvs create mode 100644 test/constraint/length_difference/test.cpp create mode 100644 test/constraint/length_ratio/normal.png create mode 100644 test/constraint/length_ratio/normal.slvs create mode 100644 test/constraint/length_ratio/normal_v20.slvs create mode 100644 test/constraint/length_ratio/normal_v22.slvs create mode 100644 test/constraint/length_ratio/reference.png create mode 100644 test/constraint/length_ratio/reference.slvs create mode 100644 test/constraint/length_ratio/reference_v20.slvs create mode 100644 test/constraint/length_ratio/reference_v22.slvs create mode 100644 test/constraint/length_ratio/test.cpp create mode 100644 test/constraint/parallel/free_in_3d.png create mode 100644 test/constraint/parallel/free_in_3d.slvs create mode 100644 test/constraint/parallel/free_in_3d_v20.slvs create mode 100644 test/constraint/parallel/free_in_3d_v22.slvs create mode 100644 test/constraint/parallel/normal.png create mode 100644 test/constraint/parallel/normal.slvs create mode 100644 test/constraint/parallel/normal_v20.slvs create mode 100644 test/constraint/parallel/normal_v22.slvs create mode 100644 test/constraint/parallel/test.cpp create mode 100644 test/constraint/perpendicular/normal.png create mode 100644 test/constraint/perpendicular/normal.slvs create mode 100644 test/constraint/perpendicular/normal_v20.slvs create mode 100644 test/constraint/perpendicular/normal_v22.slvs create mode 100644 test/constraint/perpendicular/test.cpp create mode 100644 test/constraint/points_coincident/free_in_3d.png create mode 100644 test/constraint/points_coincident/free_in_3d.slvs create mode 100644 test/constraint/points_coincident/free_in_3d_v20.slvs create mode 100644 test/constraint/points_coincident/free_in_3d_v22.slvs create mode 100644 test/constraint/points_coincident/normal.png create mode 100644 test/constraint/points_coincident/normal.slvs create mode 100644 test/constraint/points_coincident/normal_v20.slvs create mode 100644 test/constraint/points_coincident/normal_v22.slvs create mode 100644 test/constraint/points_coincident/test.cpp create mode 100644 test/constraint/proj_pt_distance/normal.png create mode 100644 test/constraint/proj_pt_distance/normal.slvs create mode 100644 test/constraint/proj_pt_distance/normal_v20.slvs create mode 100644 test/constraint/proj_pt_distance/normal_v22.slvs create mode 100644 test/constraint/proj_pt_distance/reference.png create mode 100644 test/constraint/proj_pt_distance/reference.slvs create mode 100644 test/constraint/proj_pt_distance/reference_v20.slvs create mode 100644 test/constraint/proj_pt_distance/reference_v22.slvs create mode 100644 test/constraint/proj_pt_distance/test.cpp create mode 100644 test/constraint/pt_face_distance/normal.png create mode 100644 test/constraint/pt_face_distance/normal.slvs create mode 100644 test/constraint/pt_face_distance/normal_v20.slvs create mode 100644 test/constraint/pt_face_distance/normal_v22.slvs create mode 100644 test/constraint/pt_face_distance/reference.png create mode 100644 test/constraint/pt_face_distance/reference.slvs create mode 100644 test/constraint/pt_face_distance/reference_v20.slvs create mode 100644 test/constraint/pt_face_distance/reference_v22.slvs create mode 100644 test/constraint/pt_face_distance/test.cpp create mode 100644 test/constraint/pt_in_plane/normal.png create mode 100644 test/constraint/pt_in_plane/normal.slvs create mode 100644 test/constraint/pt_in_plane/normal_v20.slvs create mode 100644 test/constraint/pt_in_plane/normal_v22.slvs create mode 100644 test/constraint/pt_in_plane/test.cpp create mode 100644 test/constraint/pt_line_distance/extended.png create mode 100644 test/constraint/pt_line_distance/extended.slvs create mode 100644 test/constraint/pt_line_distance/free_in_3d.png create mode 100644 test/constraint/pt_line_distance/free_in_3d.slvs create mode 100644 test/constraint/pt_line_distance/free_in_3d_v20.slvs create mode 100644 test/constraint/pt_line_distance/free_in_3d_v22.slvs create mode 100644 test/constraint/pt_line_distance/normal.png create mode 100644 test/constraint/pt_line_distance/normal.slvs create mode 100644 test/constraint/pt_line_distance/normal_v20.slvs create mode 100644 test/constraint/pt_line_distance/normal_v22.slvs create mode 100644 test/constraint/pt_line_distance/reference.png create mode 100644 test/constraint/pt_line_distance/reference.slvs create mode 100644 test/constraint/pt_line_distance/reference_v20.slvs create mode 100644 test/constraint/pt_line_distance/reference_v22.slvs create mode 100644 test/constraint/pt_line_distance/test.cpp create mode 100644 test/constraint/pt_on_circle/negative_dia.slvs create mode 100644 test/constraint/pt_on_circle/normal.png create mode 100644 test/constraint/pt_on_circle/normal.slvs create mode 100644 test/constraint/pt_on_circle/normal_v20.slvs create mode 100644 test/constraint/pt_on_circle/normal_v22.slvs create mode 100644 test/constraint/pt_on_circle/test.cpp create mode 100644 test/constraint/pt_on_face/normal.png create mode 100644 test/constraint/pt_on_face/normal.slvs create mode 100644 test/constraint/pt_on_face/normal_v20.slvs create mode 100644 test/constraint/pt_on_face/normal_v22.slvs create mode 100644 test/constraint/pt_on_face/test.cpp create mode 100644 test/constraint/pt_on_line/free_in_3d_v20.slvs create mode 100644 test/constraint/pt_on_line/free_in_3d_v22.slvs create mode 100644 test/constraint/pt_on_line/left_free_in_3d.png create mode 100644 test/constraint/pt_on_line/left_free_in_3d.slvs create mode 100644 test/constraint/pt_on_line/normal.png create mode 100644 test/constraint/pt_on_line/normal.slvs create mode 100644 test/constraint/pt_on_line/normal_v20.slvs create mode 100644 test/constraint/pt_on_line/normal_v22.slvs create mode 100644 test/constraint/pt_on_line/right_free_in_3d.png create mode 100644 test/constraint/pt_on_line/right_free_in_3d.slvs create mode 100644 test/constraint/pt_on_line/test.cpp create mode 100644 test/constraint/pt_plane_distance/normal.png create mode 100644 test/constraint/pt_plane_distance/normal.slvs create mode 100644 test/constraint/pt_plane_distance/normal_v20.slvs create mode 100644 test/constraint/pt_plane_distance/normal_v22.slvs create mode 100644 test/constraint/pt_plane_distance/reference.png create mode 100644 test/constraint/pt_plane_distance/reference.slvs create mode 100644 test/constraint/pt_plane_distance/reference_v20.slvs create mode 100644 test/constraint/pt_plane_distance/reference_v22.slvs create mode 100644 test/constraint/pt_plane_distance/test.cpp create mode 100644 test/constraint/pt_pt_distance/free_in_3d.png create mode 100644 test/constraint/pt_pt_distance/free_in_3d.slvs create mode 100644 test/constraint/pt_pt_distance/free_in_3d_v20.slvs create mode 100644 test/constraint/pt_pt_distance/free_in_3d_v22.slvs create mode 100644 test/constraint/pt_pt_distance/normal.png create mode 100644 test/constraint/pt_pt_distance/normal.slvs create mode 100644 test/constraint/pt_pt_distance/normal_v20.slvs create mode 100644 test/constraint/pt_pt_distance/normal_v22.slvs create mode 100644 test/constraint/pt_pt_distance/reference.png create mode 100644 test/constraint/pt_pt_distance/reference.slvs create mode 100644 test/constraint/pt_pt_distance/reference_v20.slvs create mode 100644 test/constraint/pt_pt_distance/reference_v22.slvs create mode 100644 test/constraint/pt_pt_distance/test.cpp create mode 100644 test/constraint/same_orientation/normal.png create mode 100644 test/constraint/same_orientation/normal.slvs create mode 100644 test/constraint/same_orientation/normal_v20.slvs create mode 100644 test/constraint/same_orientation/normal_v22.slvs create mode 100644 test/constraint/same_orientation/same_group.png create mode 100644 test/constraint/same_orientation/same_group.slvs create mode 100644 test/constraint/same_orientation/test.cpp create mode 100644 test/constraint/symmetric/free_in_3d.png create mode 100644 test/constraint/symmetric/free_in_3d.slvs create mode 100644 test/constraint/symmetric/free_in_3d_v20.slvs create mode 100644 test/constraint/symmetric/free_in_3d_v22.slvs create mode 100644 test/constraint/symmetric/normal.png create mode 100644 test/constraint/symmetric/normal.slvs create mode 100644 test/constraint/symmetric/normal_v20.slvs create mode 100644 test/constraint/symmetric/normal_v22.slvs create mode 100644 test/constraint/symmetric/test.cpp create mode 100644 test/constraint/symmetric_horiz/normal.png create mode 100644 test/constraint/symmetric_horiz/normal.slvs create mode 100644 test/constraint/symmetric_horiz/normal_v20.slvs create mode 100644 test/constraint/symmetric_horiz/normal_v22.slvs create mode 100644 test/constraint/symmetric_horiz/test.cpp create mode 100644 test/constraint/symmetric_line/normal.png create mode 100644 test/constraint/symmetric_line/normal.slvs create mode 100644 test/constraint/symmetric_line/normal_v20.slvs create mode 100644 test/constraint/symmetric_line/normal_v22.slvs create mode 100644 test/constraint/symmetric_line/test.cpp create mode 100644 test/constraint/symmetric_vert/normal.png create mode 100644 test/constraint/symmetric_vert/normal.slvs create mode 100644 test/constraint/symmetric_vert/normal_v20.slvs create mode 100644 test/constraint/symmetric_vert/normal_v22.slvs create mode 100644 test/constraint/symmetric_vert/test.cpp create mode 100644 test/constraint/vertical/line.png create mode 100644 test/constraint/vertical/line.slvs create mode 100644 test/constraint/vertical/line_v20.slvs create mode 100644 test/constraint/vertical/line_v22.slvs create mode 100644 test/constraint/vertical/pt_pt.png create mode 100644 test/constraint/vertical/pt_pt.slvs create mode 100644 test/constraint/vertical/pt_pt_v20.slvs create mode 100644 test/constraint/vertical/pt_pt_v22.slvs create mode 100644 test/constraint/vertical/test.cpp create mode 100644 test/constraint/where_dragged/free_in_3d.png create mode 100644 test/constraint/where_dragged/free_in_3d.slvs create mode 100644 test/constraint/where_dragged/free_in_3d_v20.slvs create mode 100644 test/constraint/where_dragged/free_in_3d_v22.slvs create mode 100644 test/constraint/where_dragged/normal.png create mode 100644 test/constraint/where_dragged/normal.slvs create mode 100644 test/constraint/where_dragged/normal_v20.slvs create mode 100644 test/constraint/where_dragged/normal_v22.slvs create mode 100644 test/constraint/where_dragged/test.cpp create mode 100644 test/core/expr/test.cpp create mode 100644 test/core/locale/test.cpp create mode 100644 test/core/path/test.cpp create mode 100644 test/debugtool.cpp create mode 100644 test/group/link/normal.png create mode 100644 test/group/link/normal.slvs create mode 100644 test/group/link/normal_v20.slvs create mode 100644 test/group/link/normal_v22.slvs create mode 100644 test/group/link/rect_v20.slvs create mode 100644 test/group/link/test.cpp create mode 100644 test/group/translate_asy/normal.png create mode 100644 test/group/translate_asy/normal.slvs create mode 100644 test/group/translate_asy/normal_v22.slvs create mode 100644 test/group/translate_asy/test.cpp create mode 100644 test/group/translate_nd/normal.png create mode 100644 test/group/translate_nd/normal.slvs create mode 100644 test/group/translate_nd/normal_v22.slvs create mode 100644 test/group/translate_nd/test.cpp create mode 100644 test/harness.cpp create mode 100644 test/harness.h create mode 100644 test/request/arc_of_circle/normal.png create mode 100644 test/request/arc_of_circle/normal.slvs create mode 100644 test/request/arc_of_circle/normal_v20.slvs create mode 100644 test/request/arc_of_circle/normal_v22.slvs create mode 100644 test/request/arc_of_circle/test.cpp create mode 100644 test/request/circle/free_in_3d.png create mode 100644 test/request/circle/free_in_3d.slvs create mode 100644 test/request/circle/free_in_3d_dof.png create mode 100644 test/request/circle/free_in_3d_v20.slvs create mode 100644 test/request/circle/free_in_3d_v22.slvs create mode 100644 test/request/circle/normal.png create mode 100644 test/request/circle/normal.slvs create mode 100644 test/request/circle/normal_dof.png create mode 100644 test/request/circle/normal_v20.slvs create mode 100644 test/request/circle/normal_v22.slvs create mode 100644 test/request/circle/test.cpp create mode 100644 test/request/cubic/normal.png create mode 100644 test/request/cubic/normal.slvs create mode 100644 test/request/cubic/normal_v20.slvs create mode 100644 test/request/cubic/normal_v22.slvs create mode 100644 test/request/cubic/test.cpp create mode 100644 test/request/cubic_periodic/normal.png create mode 100644 test/request/cubic_periodic/normal.slvs create mode 100644 test/request/cubic_periodic/normal_v20.slvs create mode 100644 test/request/cubic_periodic/normal_v22.slvs create mode 100644 test/request/cubic_periodic/test.cpp create mode 100644 test/request/datum_point/normal.png create mode 100644 test/request/datum_point/normal.slvs create mode 100644 test/request/datum_point/normal_v20.slvs create mode 100644 test/request/datum_point/normal_v22.slvs create mode 100644 test/request/datum_point/test.cpp create mode 100644 test/request/image/drawing.png create mode 100644 test/request/image/linked.png create mode 100644 test/request/image/linked.slvs create mode 100644 test/request/image/normal.png create mode 100644 test/request/image/normal.slvs create mode 100644 test/request/image/test.cpp create mode 100644 test/request/line_segment/normal.png create mode 100644 test/request/line_segment/normal.slvs create mode 100644 test/request/line_segment/normal_v20.slvs create mode 100644 test/request/line_segment/normal_v22.slvs create mode 100644 test/request/line_segment/test.cpp create mode 100644 test/request/ttf_text/kerning.png create mode 100644 test/request/ttf_text/kerning.slvs create mode 100644 test/request/ttf_text/normal.png create mode 100644 test/request/ttf_text/normal.slvs create mode 100644 test/request/ttf_text/normal_v20.slvs create mode 100644 test/request/ttf_text/normal_v22.slvs create mode 100644 test/request/ttf_text/test.cpp create mode 100644 test/request/workplane/normal.png create mode 100644 test/request/workplane/normal.slvs create mode 100644 test/request/workplane/normal_v20.slvs create mode 100644 test/request/workplane/normal_v22.slvs create mode 100644 test/request/workplane/test.cpp create mode 100644 wishlist.txt diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..5cb786f --- /dev/null +++ b/.clang-format @@ -0,0 +1,41 @@ +--- +# Things explicitly enumerated in CONTRIBUTING.md +IndentWidth: 4 +UseTab: Never +SpaceBeforeParens: Never +BreakBeforeBraces: Attach +ColumnLimit: 100 +# Docs say 100, but some places look more like 80. +#ColumnLimit: 80 + +# Based on minimizing diff when applying clang-format +AccessModifierOffset: -4 +AlignConsecutiveAssignments: true +AlignEscapedNewlines: DontAlign +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: true +AllowShortLoopsOnASingleLine: false +AlwaysBreakTemplateDeclarations: Yes # MultiLine +BreakConstructorInitializers: BeforeColon +DerivePointerAlignment: true +FixNamespaceComments: true +IndentPPDirectives: AfterHash +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: Inner +SpaceAfterTemplateKeyword: false + +# No way to remove all space around operators as seen in much of the code I looked at. +#SpaceBeforeAssignmentOperators: false + +# Only seen some of the time (mostly just variables, not functions) +# but clang-format only has a true/false +#AlignConsecutiveDeclarations: true + + +# Would be nice to turn on eventually, maybe? +SortIncludes: false +SortUsingDeclarations: false + +# Hard to tell what the desired config here was. +# AllowShortFunctionsOnASingleLine: Inline +AllowShortFunctionsOnASingleLine: None diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1f15ca3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +* text=auto +*.cpp text +*.h text +*.txt text +*.gz binary +*.ico binary +*.jpg binary +*.lib binary +*.png binary +*.slvs binary diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..640e5b5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,17 @@ +### System information + +- **SolveSpace version:** +- **Operating system:** + +### Expected behavior + + + +### Actual behavior + + + +### Additional information + + diff --git a/.github/scripts/build-macos.sh b/.github/scripts/build-macos.sh new file mode 100755 index 0000000..7111afd --- /dev/null +++ b/.github/scripts/build-macos.sh @@ -0,0 +1,51 @@ +#!/bin/sh -xe + +ENABLE_SANITIZERS="OFF" +if [ "$1" = "release" ]; then + BUILD_TYPE="RelWithDebInfo" + ENABLE_LTO="ON" +else + BUILD_TYPE="Debug" + ENABLE_LTO="OFF" +fi + +# this is an option for our Github CI only, since it doesn't have a macos arm64 image yet +CMAKE_GENERATOR="Unix Makefiles" +CMAKE_PREFIX_PATH="" +if [ "$2" = "arm64" ]; then + OSX_ARCHITECTURE="arm64" + CMAKE_PREFIX_PATH=$(find /tmp/libomp-arm64/libomp -depth 1) + mkdir build-arm64 || true + cd build-arm64 +elif [ "$2" = "x86_64" ]; then + OSX_ARCHITECTURE="x86_64" + CMAKE_PREFIX_PATH=$(find /tmp/libomp-x86_64/libomp -depth 1) + mkdir build || true + cd build +else + mkdir build || true + cd build +fi + +if [ "$3" = "xcode" ]; then + CMAKE_GENERATOR="Xcode" +fi + +cmake \ + -G "${CMAKE_GENERATOR}" \ + -D CMAKE_PREFIX_PATH="${CMAKE_PREFIX_PATH}" \ + -D CMAKE_OSX_ARCHITECTURES="${OSX_ARCHITECTURE}" \ + -D CMAKE_BUILD_TYPE="${BUILD_TYPE}" \ + -D ENABLE_OPENMP="ON" \ + -D ENABLE_SANITIZERS="${ENABLE_SANITIZERS}" \ + -D ENABLE_LTO="${ENABLE_LTO}" \ + .. + +if [ "$3" = "xcode" ]; then + open solvespace.xcodeproj +else + cmake --build . --config "${BUILD_TYPE}" -j$(sysctl -n hw.logicalcpu) + if [ $(uname -m) = "$2" ]; then + make -j$(sysctl -n hw.logicalcpu) test_solvespace + fi +fi diff --git a/.github/scripts/build-snap.sh b/.github/scripts/build-snap.sh new file mode 100755 index 0000000..665e561 --- /dev/null +++ b/.github/scripts/build-snap.sh @@ -0,0 +1,3 @@ +#!/bin/sh -xe + +./pkg/snap/build.sh --use-lxd diff --git a/.github/scripts/build-ubuntu.sh b/.github/scripts/build-ubuntu.sh new file mode 100755 index 0000000..4c8f4ea --- /dev/null +++ b/.github/scripts/build-ubuntu.sh @@ -0,0 +1,11 @@ +#!/bin/sh -xe + +mkdir build +cd build +cmake \ + -DCMAKE_BUILD_TYPE="Debug" \ + -DENABLE_OPENMP="ON" \ + -DENABLE_SANITIZERS="ON" \ + .. +make -j$(nproc) VERBOSE=1 +make test_solvespace diff --git a/.github/scripts/build-wasmlib.sh b/.github/scripts/build-wasmlib.sh new file mode 100755 index 0000000..842d1d1 --- /dev/null +++ b/.github/scripts/build-wasmlib.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +cd .. +git clone https://github.com/emscripten-core/emsdk.git --depth 1 +cd emsdk +./emsdk install latest +./emsdk activate latest +cd ../solvespace +source ../emsdk/emsdk_env.sh +mkdir -p build-wasmlib +cd build-wasmlib +emcmake cmake .. \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DENABLE_GUI="OFF" \ + -DENABLE_CLI="OFF" \ + -DENABLE_TESTS="OFF" \ + -DENABLE_COVERAGE="OFF" \ + -DENABLE_OPENMP="OFF" \ + -DFORCE_VENDORED_Eigen3="ON" \ + -DENABLE_LTO="ON" +cmake --build . -j$(nproc) --target slvs-wasm diff --git a/.github/scripts/build-web.sh b/.github/scripts/build-web.sh new file mode 100755 index 0000000..6e61338 --- /dev/null +++ b/.github/scripts/build-web.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +if [ "$1" = "release" ]; then + BUILD_TYPE="Release" + ENABLE_LTO="ON" +else + BUILD_TYPE="Debug" + ENABLE_LTO="OFF" +fi + +cd .. +git clone https://github.com/emscripten-core/emsdk.git --depth 1 +cd emsdk +./emsdk install latest +./emsdk activate latest +cd ../solvespace +source ../emsdk/emsdk_env.sh +mkdir build +cd build +emcmake cmake .. \ + -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ + -DENABLE_CLI="OFF" \ + -DENABLE_TESTS="OFF" \ + -DENABLE_COVERAGE="OFF" \ + -DENABLE_OPENMP="OFF" \ + -DENABLE_LTO="ON" +cmake --build . --config "${BUILD_TYPE}" -j$(nproc) --target solvespace diff --git a/.github/scripts/build-windows.sh b/.github/scripts/build-windows.sh new file mode 100755 index 0000000..603ada3 --- /dev/null +++ b/.github/scripts/build-windows.sh @@ -0,0 +1,55 @@ +#!/bin/sh -xe + +mkdir build +cd build + +if [ "$1" = "release" ]; then + if [ "$2" = "openmp" ]; then + ENABLE_OPENMP="ON" + else + ENABLE_OPENMP="OFF" + fi + + if [ "$3" = "x64" ]; then + PLATFORM="$3" + else + PLATFORM="Win32" + fi + + BUILD_TYPE=RelWithDebInfo + cmake \ + -G "Visual Studio 17 2022" \ + -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ + -DENABLE_OPENMP="${ENABLE_OPENMP}" \ + -DENABLE_LTO=ON \ + -DCMAKE_GENERATOR_PLATFORM="${PLATFORM}" \ + .. +else + BUILD_TYPE=Debug + cmake \ + -G "Visual Studio 17 2022" \ + -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ + -DENABLE_OPENMP="ON" \ + -DENABLE_SANITIZERS="ON" \ + -DCMAKE_GENERATOR_PLATFORM="Win32" \ + .. +fi + +cmake --build . --config "${BUILD_TYPE}" -- -maxcpucount + +# Run the tests in the proper environment (required for having the ASan libraries available) +cmake --build . --config "${BUILD_TYPE}" -t test_solvespace -- -maxcpucount + +if [ "$3" = "x64" ]; then + if [ "$2" != "openmp" ]; then + mv bin/$BUILD_TYPE/solvespace.exe bin/$BUILD_TYPE/solvespace_single_core_x64.exe + else + mv bin/$BUILD_TYPE/solvespace.exe bin/$BUILD_TYPE/solvespace_x64.exe + fi +else + if [ "$2" != "openmp" ]; then + mv bin/$BUILD_TYPE/solvespace.exe bin/$BUILD_TYPE/solvespace_single_core_x86.exe + else + mv bin/$BUILD_TYPE/solvespace.exe bin/$BUILD_TYPE/solvespace_x86.exe + fi +fi diff --git a/.github/scripts/install-macos.sh b/.github/scripts/install-macos.sh new file mode 100755 index 0000000..9388cd1 --- /dev/null +++ b/.github/scripts/install-macos.sh @@ -0,0 +1,22 @@ +#!/bin/sh -xe +set -o pipefail + +if [ "$1" = "ci" ]; then + brew_cache=$(brew --cache) + brew fetch --bottle-tag=arm64_tahoe libomp >/dev/null + brew fetch --bottle-tag=sonoma libomp >/dev/null + armloc=$(find "$brew_cache"/downloads -maxdepth 1 -name "*--libomp--*arm64_tahoe.bottle.tar.gz" -print | tail -n1) + x64loc=$(find "$brew_cache"/downloads -maxdepth 1 -name "*--libomp--*sonoma.bottle.tar.gz" -print | tail -n1) + [ -n "$armloc" ] || { echo "Failed to locate arm64 libomp bottle in cache" >&2; exit 1; } + [ -n "$x64loc" ] || { echo "Failed to locate x86_64 libomp bottle in cache" >&2; exit 1; } + cp "$armloc" /tmp/libomp-arm64.tar.gz + mkdir /tmp/libomp-arm64 || true + tar -xzvf /tmp/libomp-arm64.tar.gz -C /tmp/libomp-arm64 + cp "$x64loc" /tmp/libomp-x86_64.tar.gz + mkdir /tmp/libomp-x86_64 || true + tar -xzvf /tmp/libomp-x86_64.tar.gz -C /tmp/libomp-x86_64 +else + brew install libomp +fi + +git submodule update --init extlib/cairo extlib/freetype extlib/libdxfrw extlib/libpng extlib/mimalloc extlib/pixman extlib/zlib extlib/eigen diff --git a/.github/scripts/install-snap.sh b/.github/scripts/install-snap.sh new file mode 100755 index 0000000..e10a91d --- /dev/null +++ b/.github/scripts/install-snap.sh @@ -0,0 +1,6 @@ +#!/bin/sh -xe + +sudo /snap/bin/lxd waitready +sudo /snap/bin/lxd init --auto +sudo chgrp travis /var/snap/lxd/common/lxd/unix.socket +mkdir -p "$TRAVIS_BUILD_DIR/snaps-cache" diff --git a/.github/scripts/install-ubuntu.sh b/.github/scripts/install-ubuntu.sh new file mode 100755 index 0000000..6033f73 --- /dev/null +++ b/.github/scripts/install-ubuntu.sh @@ -0,0 +1,10 @@ +#!/bin/sh -xe + +sudo apt-get update -qq + +sudo apt-get install -q -y \ + zlib1g-dev libpng-dev libcairo2-dev libfreetype6-dev libjson-c-dev \ + libfontconfig1-dev libgtkmm-3.0-dev libpangomm-1.4-dev libgl-dev \ + libgl-dev libglu-dev libspnav-dev + +git submodule update --init extlib/libdxfrw extlib/mimalloc extlib/eigen diff --git a/.github/scripts/install-web.sh b/.github/scripts/install-web.sh new file mode 100755 index 0000000..738d67d --- /dev/null +++ b/.github/scripts/install-web.sh @@ -0,0 +1,3 @@ +#!/bin/sh -xe + +git submodule update --init diff --git a/.github/scripts/install-windows.sh b/.github/scripts/install-windows.sh new file mode 100755 index 0000000..738d67d --- /dev/null +++ b/.github/scripts/install-windows.sh @@ -0,0 +1,3 @@ +#!/bin/sh -xe + +git submodule update --init diff --git a/.github/scripts/sign-macos.sh b/.github/scripts/sign-macos.sh new file mode 100755 index 0000000..63f5734 --- /dev/null +++ b/.github/scripts/sign-macos.sh @@ -0,0 +1,86 @@ +#!/bin/bash -xe + +lipo \ + -create \ + build/bin/SolveSpace.app/Contents/Resources/libomp.dylib \ + build-arm64/bin/SolveSpace.app/Contents/Resources/libomp.dylib \ + -output \ + build/bin/SolveSpace.app/Contents/Resources/libomp.dylib + +lipo \ + -create \ + build/bin/SolveSpace.app/Contents/MacOS/SolveSpace \ + build-arm64/bin/SolveSpace.app/Contents/MacOS/SolveSpace \ + -output \ + build/bin/SolveSpace.app/Contents/MacOS/SolveSpace + +lipo \ + -create \ + build/bin/SolveSpace.app/Contents/MacOS/solvespace-cli \ + build-arm64/bin/SolveSpace.app/Contents/MacOS/solvespace-cli \ + -output \ + build/bin/SolveSpace.app/Contents/MacOS/solvespace-cli + +cd build + +openmp="bin/SolveSpace.app/Contents/Resources/libomp.dylib" +app="bin/SolveSpace.app" +dmg="bin/SolveSpace.dmg" +bundle_id="com.solvespace.solvespace" + +if [ "$CI" = "true" ]; then + # get the signing certificate (this is the Developer ID:Application: Your Name, exported to a p12 file, then converted to base64, e.g.: cat ~/Desktop/certificate.p12 | base64 | pbcopy) + echo $MACOS_CERTIFICATE_P12 | base64 --decode > certificate.p12 + + # create a keychain + security create-keychain -p secret build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p secret build.keychain + + # import the key + security import certificate.p12 -k build.keychain -P "${MACOS_CERTIFICATE_PASSWORD}" -T /usr/bin/codesign + + security set-key-partition-list -S apple-tool:,apple: -s -k secret build.keychain + + # check if all is good + security find-identity -v +fi + +# sign openmp +codesign -s "${MACOS_DEVELOPER_ID}" --timestamp --options runtime -f --deep "${openmp}" + +# sign the .app +codesign -s "${MACOS_DEVELOPER_ID}" --timestamp --options runtime -f --deep "${app}" + +# create the .dmg from the signed .app +hdiutil create -srcfolder "${app}" "${dmg}" + +# sign the .dmg +codesign -s "${MACOS_DEVELOPER_ID}" --timestamp --options runtime -f --deep "${dmg}" + +if ! command -v xcrun >/dev/null || ! xcrun --find notarytool >/dev/null; then + echo "Notarytool is not present in the system. Notarization has failed." + exit 1 +fi + +# Submit the package for notarization +notarization_output=$( + xcrun notarytool submit "${dmg}" \ + --apple-id "hello@koenschmeets.nl" \ + --password "${MACOS_APPSTORE_APP_PASSWORD}" \ + --team-id "8X77K9NDG3" \ + --wait 2>&1) + +if [ $? -eq 0 ]; then + # Extract the operation ID from the output + operation_id=$(echo "$notarization_output" | awk '/RequestUUID/ {print $NF}') + echo "Notarization submitted. Operation ID: $operation_id" + exit 0 + else + echo "Notarization failed. Error: $notarization_output" + exit 1 + fi +fi + +# staple +xcrun stapler staple "${dmg}" diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..dd1ae04 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,225 @@ +name: CD + +on: + push: + branches: + - master + release: + types: + - created + +jobs: + cancel_previous_runs: + runs-on: ubuntu-latest + name: Cancel Previous Runs + permissions: + actions: write + steps: + - uses: styfle/cancel-workflow-action@0.13.1 + with: + access_token: ${{ github.token }} + + test_ubuntu: + needs: [cancel_previous_runs] + runs-on: ubuntu-latest + name: Test Ubuntu + steps: + - uses: actions/checkout@v6 + - name: Install Dependencies + run: .github/scripts/install-ubuntu.sh + - name: Build & Test + run: .github/scripts/build-ubuntu.sh + + test_windows: + needs: [cancel_previous_runs] + runs-on: windows-2022 + name: Test Windows + steps: + - uses: actions/checkout@v6 + - name: Install Dependencies + run: .github/scripts/install-windows.sh + shell: bash + - name: Build & Test + run: .github/scripts/build-windows.sh + shell: bash + + test_macos: + needs: [cancel_previous_runs] + runs-on: macos-latest + name: Test macOS + steps: + - uses: actions/checkout@v6 + - name: Install Dependencies + run: .github/scripts/install-macos.sh ci + - name: Build & Test + run: .github/scripts/build-macos.sh debug arm64 && .github/scripts/build-macos.sh debug x86_64 + + build_release_windows_x86: + needs: [test_ubuntu, test_windows, test_macos] + name: Build Release Windows x86 + runs-on: windows-2022 + steps: + - uses: actions/checkout@v6 + - name: Install Dependencies + run: .github/scripts/install-windows.sh + shell: bash + - name: Build & Test + run: .github/scripts/build-windows.sh release + shell: bash + - name: Upload artifact + uses: actions/upload-artifact@v6 + with: + name: windows_single_core_x86 + path: build/bin/RelWithDebInfo/solvespace_single_core_x86.exe + + build_release_windows_openmp_x86: + needs: [test_ubuntu, test_windows, test_macos] + name: Build Release Windows (OpenMP) x86 + runs-on: windows-2022 + steps: + - uses: actions/checkout@v6 + - name: Install Dependencies + run: .github/scripts/install-windows.sh + shell: bash + - name: Build & Test + run: .github/scripts/build-windows.sh release openmp + shell: bash + - name: Upload artifact + uses: actions/upload-artifact@v6 + with: + name: windows_x86 + path: build/bin/RelWithDebInfo/solvespace_x86.exe + + build_release_windows_x64: + needs: [test_ubuntu, test_windows, test_macos] + name: Build Release Windows x64 + runs-on: windows-2022 + steps: + - uses: actions/checkout@v6 + - name: Install Dependencies + run: .github/scripts/install-windows.sh + shell: bash + - name: Build & Test + run: .github/scripts/build-windows.sh release not_openmp x64 + shell: bash + - name: Upload artifact + uses: actions/upload-artifact@v6 + with: + name: windows_single_core_x64 + path: build/bin/RelWithDebInfo/solvespace_single_core_x64.exe + + build_release_windows_openmp_x64: + needs: [test_ubuntu, test_windows, test_macos] + name: Build Release Windows (OpenMP) x64 + runs-on: windows-2022 + steps: + - uses: actions/checkout@v6 + - name: Install Dependencies + run: .github/scripts/install-windows.sh + shell: bash + - name: Build & Test + run: .github/scripts/build-windows.sh release openmp x64 + shell: bash + - name: Upload artifact + uses: actions/upload-artifact@v6 + with: + name: windows_x64 + path: build/bin/RelWithDebInfo/solvespace_x64.exe + + build_release_macos: + needs: [test_ubuntu, test_windows, test_macos] + name: Build Release macOS + runs-on: macos-latest + steps: + - uses: actions/checkout@v6 + - name: Install Dependencies + run: .github/scripts/install-macos.sh ci + - name: Build & Test + run: .github/scripts/build-macos.sh release arm64 && .github/scripts/build-macos.sh release x86_64 + - name: Sign Build + run: .github/scripts/sign-macos.sh + env: + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + MACOS_CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }} + MACOS_APPSTORE_APP_PASSWORD: ${{ secrets.MACOS_APPSTORE_APP_PASSWORD }} + MACOS_APPSTORE_USERNAME: ${{ secrets.MACOS_APPSTORE_USERNAME }} + MACOS_DEVELOPER_ID: ${{ secrets.MACOS_DEVELOPER_ID }} + - name: Upload artifact + uses: actions/upload-artifact@v6 + with: + name: macos + path: build/bin/SolveSpace.dmg + + build_release_web: + needs: [test_ubuntu, test_windows, test_macos] + name: Build Release Web + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Install Dependencies + run: .github/scripts/install-web.sh + - name: Build & Pack + run: | + .github/scripts/build-web.sh release + cd build/bin + rm *.a || true + - name: Upload artifact + uses: actions/upload-artifact@v6 + with: + name: solvespace_web + path: build/bin/ + + # deploy_snap_amd64: + # needs: [test_ubuntu, test_windows, test_macos] + # name: Deploy AMD64 Snap + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v6 + # - name: Fetch Tags + # run: git fetch --force --tags + # - name: Set Up Source + # run: rsync --filter=":- .gitignore" -r ./ pkg/snap/solvespace-snap-src + # - name: Build Snap + # uses: snapcore/action-build@v1 + # id: build + # with: + # path: pkg/snap + # - name: Upload & Release to Edge + # if: github.event_name == 'push' + # uses: snapcore/action-publish@v1 + # env: + # SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPSTORE_LOGIN }} + # with: + # snap: ${{ steps.build.outputs.snap }} + # release: edge + # - name: Upload & Release to Beta + Edge + # if: github.event_name == 'release' + # uses: snapcore/action-publish@v1 + # with: + # store_login: ${{ secrets.SNAPSTORE_LOGIN }} + # snap: ${{ steps.build.outputs.snap }} + # release: edge,beta + + upload_release_assets: + name: Upload Release Assets + needs: + - build_release_windows_x86 + - build_release_windows_openmp_x86 + - build_release_windows_x64 + - build_release_windows_openmp_x64 + - build_release_macos + if: "!cancelled() && github.event_name == 'release'" + runs-on: ubuntu-latest + steps: + - name: Download All Workflow Artifacts + uses: actions/download-artifact@v4 + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + files: | + windows_single_core_x86/solvespace_single_core_x86.exe + windows_x86/solvespace_x86.exe + windows_single_core_x64/solvespace_single_core_x64.exe + windows_x64/solvespace_x64.exe + macos/SolveSpace.dmg \ No newline at end of file diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..5e69eec --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,181 @@ +name: Python + +on: + push: + branches: [ master ] + tags: [ v* ] + workflow_dispatch: + +jobs: + python-sdist: + name: python sdist + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - run: git submodule update --init extlib/mimalloc extlib/eigen + - uses: actions/setup-python@v6 + with: + python-version: "3.10" + - name: Pack + shell: bash + run: | + if [[ "${GITHUB_REF}" == refs/tags/* ]]; then + version="${GITHUB_REF##*/}" + else + mkdir empty-build + cd empty-build + cmake .. -DENABLE_GUI=OFF -DENABLE_CLI=OFF -DENABLE_TESTS=OFF -DENABLE_COVERAGE=OFF -DENABLE_SANITIZERS=OFF -DENABLE_OPENMP=OFF + source version.env + cd .. + version="${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}.dev${GITHUB_RUN_NUMBER}" + fi + sed -i.bak "s/^version = .*/version = \"${version}\"/g" pyproject.toml && rm pyproject.toml.bak + python -m pip install -U setuptools build + python -m build --sdist + - name: Upload artifact + uses: actions/upload-artifact@v6 + with: + name: sdist + path: dist/*.tar.gz + python-wheel: + name: ${{ matrix.os_short }} python ${{ matrix.architecture }} cp${{ matrix.python_version }} + runs-on: ${{ matrix.os }} + timeout-minutes: 240 + strategy: + fail-fast: false + matrix: + os_arch: [ + "windows-ia32", + "windows-x64", + # "windows-arm64", + "macos-x86_64", + "macos-arm64", + "linux-x86_64", + "linux-aarch64", + ] + python_version: [ + "37", + "38", + "39", + "310", + "311", + "312", + "313", + "314", + "315", + ] + exclude: + - os_arch: "macos-arm64" + python_version: "37" + include: + - os_arch: "windows-ia32" + os: "windows-2022" + os_short: "windows" + architecture: "ia32" + cibuildwheel_architecture: "x86" + cmake_generator_platform: "Win32" + - os_arch: "windows-x64" + os: "windows-2022" + os_short: "windows" + architecture: "x64" + cibuildwheel_architecture: "AMD64" + cmake_generator_platform: "x64" + # - os_arch: "windows-arm64" + # os: "windows-2022" + # os_short: "windows" + # architecture: "arm64" + # cibuildwheel_architecture: "ARM64" + # cmake_generator_platform: "ARM64" + - os_arch: "macos-x86_64" + os: "macos-latest" + os_short: "macos" + cibuildwheel_architecture: "x86_64" + architecture: "x86_64" + - os_arch: "macos-arm64" + os: "macos-latest" + os_short: "macos" + cibuildwheel_architecture: "arm64" + architecture: "arm64" + - os_arch: linux-x86_64 + os: "ubuntu-22.04" + os_short: "linux" + cibuildwheel_architecture: "x86_64" + architecture: "x86_64" + - os_arch: linux-aarch64 + os: "ubuntu-22.04" + os_short: "linux" + cibuildwheel_architecture: "aarch64" + architecture: "aarch64" + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - run: git submodule update --init extlib/mimalloc extlib/eigen + - name: Set version + shell: bash + run: | + if [[ "${GITHUB_REF}" == refs/tags/* ]]; then + version="${GITHUB_REF##*/}" + else + mkdir empty-build + cd empty-build + cmake .. -DENABLE_GUI=OFF -DENABLE_CLI=OFF -DENABLE_TESTS=OFF -DENABLE_COVERAGE=OFF -DENABLE_SANITIZERS=OFF -DENABLE_OPENMP=OFF + source version.env + cd .. + version="${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}.dev${GITHUB_RUN_NUMBER}" + fi + sed -i.bak "s/^version = .*/version = \"${version}\"/g" pyproject.toml && rm pyproject.toml.bak + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + if: matrix.architecture == 'aarch64' + with: + platforms: arm64 + - name: Build wheels + uses: pypa/cibuildwheel@v2.22.0 + env: + CIBW_BUILD: "cp${{ matrix.python_version }}-*" + CIBW_PLATFORM: "${{ matrix.os_short }}" + CIBW_BUILD_VERBOSITY: "1" + CIBW_ARCHS: "${{ matrix.cibuildwheel_architecture }}" + CIBW_ENVIRONMENT_WINDOWS: > + CMAKE_GENERATOR="Visual Studio 17 2022" + CMAKE_GENERATOR_PLATFORM="${{ matrix.cmake_generator_platform }}" + CIBW_ENVIRONMENT_PASS_WINDOWS: "CMAKE_GENERATOR CMAKE_GENERATOR_PLATFORM" + CIBW_ENVIRONMENT_MACOS: MACOSX_DEPLOYMENT_TARGET=10.12 + - uses: actions/upload-artifact@v6 + with: + name: wheel-cp${{ matrix.python_version }}-${{ matrix.os_short }}-${{ matrix.architecture }} + path: | + ./wheelhouse/*.whl + publish-pypi: + name: publish to PyPi + needs: [ + python-sdist, + python-wheel, + ] + runs-on: ubuntu-22.04 + steps: + - uses: actions/download-artifact@v4 + with: + path: prebuilds + - name: prepare + shell: bash + run: | + mkdir dist + ls prebuilds + mv prebuilds/*/* dist + ls dist + - name: Publish wheels to Test PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.TEST_PYPI_API_TOKEN }} + repository-url: https://test.pypi.org/legacy/ + attestations: false + verbose: true + - name: Publish wheels to PyPI + if: startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} + attestations: false + verbose: true diff --git a/.github/workflows/source-tarball.yml b/.github/workflows/source-tarball.yml new file mode 100644 index 0000000..1367c37 --- /dev/null +++ b/.github/workflows/source-tarball.yml @@ -0,0 +1,56 @@ +name: Source Tarball + +on: + release: + types: + - created + +jobs: + create_tarball: + name: Create & Upload Tarball + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: true + fetch-depth: 0 + - name: Pack Tarball + id: pack_tarball + run: | + version="${GITHUB_REF#refs/tags/v}" + dir_name="solvespace-${version}" + archive_name="${dir_name}.tar.xz" + archive_path="${HOME}/${archive_name}" + commit_sha="$GITHUB_SHA" + + sed -e 's/^\(include(GetGitCommitHash)\)/#\1/' \ + -e 's/^# \(set(GIT_COMMIT_HASH\).*/\1 '"$commit_sha"')/' \ + -i CMakeLists.txt + + echo "::set-output name=archive_name::${archive_name}" + echo "::set-output name=archive_path::${archive_path}" + + cd .. + tar \ + --exclude-vcs \ + --transform "s:^solvespace:${dir_name}:" \ + -cvaf \ + ${archive_path} \ + solvespace + - name: Get Release Upload URL + id: get_upload_url + env: + event: ${{ toJson(github.event) }} + run: | + upload_url=$(echo "$event" | jq -r ".release.upload_url") + echo "::set-output name=upload_url::$upload_url" + echo "Upload URL: $upload_url" + - name: Upload Tarball + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.get_upload_url.outputs.upload_url }} + asset_path: ${{ steps.pack_tarball.outputs.archive_path }} + asset_name: ${{ steps.pack_tarball.outputs.archive_name }} + asset_content_type: binary/octet-stream diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..d700e8c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,61 @@ +name: Test + +on: + pull_request: + branches: + - master + push: + branches-ignore: + - master + tags-ignore: + - v* + +jobs: + test_ubuntu: + runs-on: ubuntu-latest + name: Test Ubuntu + steps: + - uses: actions/checkout@v6 + - name: Install Dependencies + run: .github/scripts/install-ubuntu.sh + - name: Build & Test + run: .github/scripts/build-ubuntu.sh + + test_windows: + runs-on: windows-2022 + name: Test Windows + steps: + - uses: actions/checkout@v6 + - name: Install Dependencies + run: .github/scripts/install-windows.sh + shell: bash + - name: Build & Test + run: .github/scripts/build-windows.sh + shell: bash + + test_macos: + runs-on: macos-latest + name: Test macOS + steps: + - uses: actions/checkout@v6 + - name: Install Dependencies + run: .github/scripts/install-macos.sh ci + - name: Build & Test + run: .github/scripts/build-macos.sh debug arm64 && .github/scripts/build-macos.sh debug x86_64 + + test_flatpak: + name: Test Flatpak x86_64 + runs-on: ubuntu-latest + container: + image: bilelmoussaoui/flatpak-github-actions:freedesktop-21.08 + options: --privileged + steps: + - uses: actions/checkout@v6 + with: + submodules: true + fetch-depth: 0 + - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 + with: + bundle: "solvespace.flatpak" + manifest-path: "pkg/flatpak/com.solvespace.SolveSpace.json" + cache-key: flatpak-builder-${{ github.sha }} diff --git a/.github/workflows/wasmlib.yml b/.github/workflows/wasmlib.yml new file mode 100644 index 0000000..afc9666 --- /dev/null +++ b/.github/workflows/wasmlib.yml @@ -0,0 +1,79 @@ +name: WASM Library + +on: + push: + branches: [ master ] + tags: [ v* ] + workflow_dispatch: + +# https://docs.npmjs.com/trusted-publishers +permissions: + id-token: write + contents: read + +jobs: + build-wasmlib: + name: WASM library + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - run: git submodule update --init extlib/mimalloc extlib/eigen + - name: Pack + shell: bash + run: | + .github/scripts/build-wasmlib.sh + cd build-wasmlib/bin + rm libmimalloc.a || true + zip -r slvs-wasmlib.zip . + - name: Upload artifact + uses: actions/upload-artifact@v6 + with: + name: slvs-wasmlib + path: build-wasmlib/bin/slvs-wasmlib.zip + publish-wasmlib: + name: publish WASM library + needs: [ + build-wasmlib, + ] + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v6 + - run: git submodule update --init extlib/mimalloc extlib/eigen + - uses: actions/download-artifact@v4 + with: + path: artifacts + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: '24.x' + registry-url: https://registry.npmjs.org/ + - name: prepare + shell: bash + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + git fetch --unshallow --tags + + ls artifacts + unzip artifacts/slvs-wasmlib/slvs-wasmlib.zip -d js + ls js + + mkdir empty-build + cd empty-build + cmake .. -DENABLE_GUI=OFF -DENABLE_CLI=OFF -DENABLE_TESTS=OFF -DENABLE_COVERAGE=OFF -DENABLE_SANITIZERS=OFF -DENABLE_OPENMP=OFF + source version.env + cd .. + if [[ "${GITHUB_REF}" == refs/tags/* ]]; then + version="${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}" + else + version="${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}-dev.${GITHUB_RUN_NUMBER}" + is_dev="1" + fi + jq --arg version "${version}" '.version = $version' package.json > package.json.tmp + mv package.json.tmp js/package.json + cd js + if [ "${is_dev}" == "1" ]; then + npm publish --access public --tag dev + else + npm publish --access public --tag stable + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bf6e0f --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +/CMakeCache.txt +/build*/ +/test/**/*.diff.* +/test/**/*.curr.* +*.trace +/debian/tmp/ +/debian/*.log +/debian/*.substvars +/debian/*.debhelper +/debian/files +/debian/solvespace/ +/debian/libslvs1/ +/debian/libslvs1-dev/ +/obj-*/ +/*.slvs +.vscode/ +.cache/ + +# Visual Studio +out/ +.vs/ +CMakeSettings.json +__pycache__/ +solvespace.egg-info/ +dist/ +.DS_Store diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..84b25a8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,28 @@ +[submodule "extlib/zlib"] + path = extlib/zlib + url = https://github.com/madler/zlib + ignore = dirty +[submodule "extlib/libpng"] + path = extlib/libpng + url = https://github.com/glennrp/libpng +[submodule "extlib/freetype"] + path = extlib/freetype + url = https://github.com/freetype/freetype.git +[submodule "extlib/libdxfrw"] + path = extlib/libdxfrw + url = https://github.com/solvespace/libdxfrw.git +[submodule "extlib/pixman"] + path = extlib/pixman + url = https://github.com/solvespace/pixman +[submodule "extlib/cairo"] + path = extlib/cairo + url = https://github.com/solvespace/cairo +[submodule "extlib/angle"] + path = extlib/angle + url = https://github.com/solvespace/angle +[submodule "extlib/mimalloc"] + path = extlib/mimalloc + url = https://github.com/microsoft/mimalloc +[submodule "extlib/eigen"] + path = extlib/eigen + url = https://gitlab.com/libeigen/eigen.git diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..2264671 --- /dev/null +++ b/.mailmap @@ -0,0 +1,25 @@ +Bauke Conijn +Bauke Conijn + +EvilSpirit + +Isaac Garzón +Isaac Garzón +Isaac Garzón +Isaac Garzón + +luz paz + +Maximilian Federle +Maximilian Federle + +Not4Sure + +Paul Kahler <14852918+phkahler@users.noreply.github.com> + +Rylie Pavlik +Rylie Pavlik +Rylie Pavlik +Rylie Pavlik +Rylie Pavlik +Rylie Pavlik diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d9d9a0f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,434 @@ +Changelog +========= + +3.2 +--- + +Geometric Modelling Kernel (NURBS) + +* Improve the difference boolean operations. +* Extrude groups have a checkbox to allow skewing - 3 DoF for the extrude direction. + +Constraints (new and improved): + +* Add Parallel and Perpendicular constraints for 2 faces. +* The equal angle constraint is moved to the `N` shortcut and menu item to allow equal length (`Q`) to be applied to three or four lines. +* Allow tangent on arcs/lines/splines without coincident endpoints. + +Allow these constraints to be applied to more entities at once: + +* More than two line Segments - equal length. +* More than two Arcs and/or circles - equal diameter/radius. +* Any number of Lines - horizontal or vertical. +* More than two points - horizontal or vertical. +* Point on face can be applied to a point and 1-3 faces at once. +* More than two points coincident. + +Sketching + +* `Image` sketch elements are not copied in 3d groups (extrude, lathe, revolve, helix) by default. `Toggle Construction` for an image to get the old behavior. +* New option to use radius instead of diameter for circle and arc dimensions. +* Dimensions on arcs default to radius instead of diameter. Uncheck `use radius` in the Property Browser to get diameters. +* `Text in True Type Font` elements now support basic kerning. Enable it with `apply kerning` in the Property Browser. + +Translations (now in 10 languages!) + +* Added Czech cs_CZ. +* Added Japanese ja_JP. +* Update translation for French fr_FR, Russian ru_RU, Chinese zh_CN and Ukrainian uk_UA. + +Other User interface changes: + +* `CTRL+Tab` hides/shows the toolbar. +* Marquee selection of line segments is now precise. +* Speed up the animation when moving the view, for example when pressing `F2` or `F3`. +* The animation speed when changing the 3D view is configurable. +* Pressing ESC while drawing a sketch entity now deletes the entity rather than completing it. +* `CTRL+Shift+S` shortcut for "Save As..." +* New option "use camera mouse navigation" for camera (instead of the default model) rotation navigation. +* Sketches can be displayed with only dimensions visible (the button controlling visibility of constraints in the Property Browser has a new state). +* More entity types described in the text screens. +* A new option to `display the full path in the title bar`; or not. +* Handle smooth scrolling (mouse wheel zoom) on GTK/Linux. +* The embedded vector font (used for the UI and labels): + * now has many new and improved Chinese characters + * includes the Greek alphabet + * improved "є" + * added superscript 5 "⁵" + * fixed crash when typing "£" +* Add an 'only unconstrained' option on the Property Browser home screen that shows only unconstrained groups. +* Some faces can be gragged. Linked objects can be moved by gragging their faces. + +Solver + +* Merged Python bindings for the geometric constraint solver to master. + +Other + +* STEP files now export as solid model(s). +* Merged a QT front end as an option for Linux builds. +* Merged and improved the experimental Web version (Emscripten port). +* Better Flatpack support. +* Several bug fixes and usability improvements. +* Allow 32 bit SolveSpace to access up to 4GB of RAM to allow working on larger projects. + +Bug fixes: + +* `Paste Transformed` on elements that contain a point-line distance does not flip any more. +* Fix saving assemblies when opened with a relative path on the command line. +* Windows: The Property Browser remains visible when going full screen (CTRL-F11). +* Fix crash when linking .STL and .EMN files with extensions in capital letters. +* Mac: Don't crash on file ops without changing dir. +* Fix incorrect arc handling when linking IDF files. +* Windows: avoid (sometimes) showing multiple messages when deleting an entity causes other entities to be removed. +* "Split Curves at Intersection" fixes: + * Does not create a zero length edge when splitting lines intersecting in a T shape + * Does not create an invisible zero length edge and jump the intersection point to (0, 0, 0) when splitting lines with coincident end points. +* Fix IDF import with THOU units. +* macOS: Fix some points not draggable on macOS 26 (Tahoe). +* Windows 10/11: Fix the button click area in the property browser shifting because the contents were rendered stretched or compressed. + +3.1 +--- + +Constraints: + +* Arcs length ratio and difference. +* Arc & Line length ratio and difference. +* Allow comments to be associated with point entities. + +Sketching: + +* Support for pan, zoom and rotate trackpad gestures on macOS +* Add "exploded view" to sketches via "\\" key. Shows sketch elements separated + by a configurable distance perpendicular to the sketch plane. +* Added Feet-Inches as a unit of measure. Inputs are still in inches. + But the display shows feet, inches, and fraction of an inch. +* Added an optional "pitch" parameter to helix extrusions (in the text window) +* Allow use of Point & Normal to define "sketch-in-new-workplane". +* Update "Property Browser" live while dragging the sketch. + +MISC: + +* Add a link to the GitHub commit from which SolveSpace was built in the Help + menu. +* Make all points, vectors and normals shown in the Property Browser into + active links. This makes them explorable and selectable. +* Load 16bit PNG images correctly by re-scaling to 8bit. +* Fixed hang when trying to display characters missing from the embedded font. +* The main window vertical size can be as small as the toolbar. +* Configurable "SafeHeight" parameter instead of the fixed 5mm for G-code export. +* Add Spanish / Argentina translation. +* Move "perspective factor", "lighting direction" and "explode distance" from + the "configuration" screen to the "view" screen. +* Add a "∆" suffix to groups which have "force to triangle mesh" ticked +* Gray the group name in the text window for groups with suppressed solid model. +* Added the ability to Link STL files. +* When linking circuit boards (IDF .emn files) show keepout regions as construction entities. + +Performance: + +* Speed up sketches with many constraints by roughly 8x by using the Eigen + library in the solver. The maximum unknowns increased from 1024 to 2048. +* Add a "suppress dof calculation" setting to groups - increases performance for + complex sketches. +* More changes to the ID list implementation. + +3.0 +--- + +New sketch features: + +* New intersection boolean operation for solid models. +* New groups, revolution and helical extrusion. +* Extrude, lathe, translate and rotate groups can use the "assembly" + boolean operation, to increase performance. +* The solid model of extrude and lathe groups can be suppressed, + for splitting a single model in multiple parts to export, + or if only the generated entities are desired, without the mesh. +* Translate and rotate groups can create n-dimensional arrays using + the "difference" and "assembly" boolean operations. +* A new sketch in workplane group can be created based on existing workplane. +* TTF text request has two additional points on the right side, which allow + constraining the width of text. +* Image requests can now be created, similar to TTF text requests. + This replaces the "style → background image" feature. +* Irrelevant points (e.g. arc center point) are not counted when estimating + the bounding box used to compute chord tolerance. +* When adding a constraint which has a label and is redundant with another + constraint, the constraint is added as a reference, avoiding an error. +* Datum points can be copied and pasted. +* "Split Curves at Intersection" can now split curves at point lying on curve, + not just at intersection of two curves. +* Property browser now shows amount of degrees of freedom in group list. + It also shows a yellow "err" if the sketch has problems (e.g. self + intersecting) that would propagate in subsequent groups. +* It is now possible to press "g" to toggle construction on new objects while + they are still being drawn. +* Allow right click to end sketching of all entities. + +New constraint features: + +* When dragging an arc or rectangle point, it will be automatically + constrained to other points with a click. +* When selecting a constraint, the requests it constraints can be selected + in the text window. +* When selecting an entity, the constraints applied to it can be selected + in the text window. +* Distance constraint labels can now be formatted to use SI prefixes. + Values are edited in the configured unit regardless of label format. +* When creating a constraint, if an exactly identical constraint already + exists, it is now selected instead of adding a redundant constraint. +* It is now possible to turn off automatic creation of horizontal/vertical + constraints on line segments. +* Automatic creation of constraints no longer happens if the constraint + would have been redundant with other ones. +* New option to open the constraint editor for newly created constraints + with a value. +* New "redundant constraint timeout (in ms)" option to prevent UI freeze + when looking for redundant constraints. +* Swap vertical and horizontal constraints when pasting rotated by 90/270 + degrees. + +New export/import features: + +* Link IDF circuit boards in an assembly (.emn files) +* Three.js: allow configuring projection for exported model, and initially + use the current viewport projection. +* Wavefront OBJ: a material file is exported alongside the model, containing + mesh color information. +* DXF/DWG: 3D DXF files are imported as construction entities, in 3d. +* VRML (WRL) triangle meshes can now be exported, useful for e.g. [KiCAD](http://kicad.org). +* Export 2d section: custom styled entities that lie in the same + plane as the exported section are included. +* Added ExportBackgroundColor in configuration for EPS, PDF, and SVG files. +* STEP export includes object colors and transparency. +* Default "line styles" have a new "export these objects" option. + +New rendering features: + +* The "Show/hide hidden lines" button is now a tri-state button that allows + showing all lines (on top of shaded mesh), stippling occluded lines + or not drawing them at all. +* The "Show/hide outlines" button is now independent from "Show/hide edges". +* "View | Darken Inactive Solids" added. When turned off and a "sketch in plane" + group is active solids form previous groups will not be "darkened" (have the + s000d-#def-dim-solid style applied to them). + +New measurement/analysis features: + +* New choice for base unit, meters. +* New command for measuring total length of selected entities, + "Analyze → Measure Perimeter". +* New command for measuring center of mass, with live updates as the sketch + changes, "Analyze → Center of Mass". +* New option for displaying areas of closed contours. +* When calculating volume of the mesh, volume of the solid from the current + group is now shown alongside total volume of all solids. +* When calculating area, and faces are selected, calculate area of those faces + instead of the closed contour in the sketch. +* When selecting a point and a line, projected distance to current + workplane is displayed. + +Other new features: + +* Improvements to the text window for selected entities and constraints. +* Ambient light source added in text window to allow flat shaded renderings. +* New command-line interface, for batch exporting and more. +* The graphical interface now supports HiDPI screens on every OS. +* New option to lock Z axis to be always vertical when rotating the view, + a.k.a. "turntable navigation". +* New button to hide all construction entities. +* New link to match the on-screen size of the sketch with its actual size, + "view → set to full scale". +* When zooming to fit, constraints are also considered. +* Ctrl-clicking entities now deselects them, as the inverse of clicking. +* When clicking on an entity that shares a place with other entities, + the entity from the current group is selected. +* When dragging an entity that shares a place with other entities, + the entity from a request is selected. For example, dragging a point on + a face of an extrusion coincident with the source sketch plane will + drag the point from the source sketch. +* The default font for TTF text is now Bitstream Vera Sans, which is + included in the resources such that it is available on any OS. +* In expressions, numbers can contain the digit group separator, "_". +* The "=" key is bound to "Zoom In", like "+" key. +* The numpad decimal separator key is bound to "." regardless of locale. +* On Windows, full-screen mode is implemented. +* On Linux, native file chooser dialog can be used. +* New edit menu items "Line Styles", "View Projection" and "Configuration" + that are shortcuts to the respective configuration screens. +* New cmake build options using -DENABLE_OPENMP=yes and -DENABLE_LTO=yes + to enable support for multi-threading and link-time optimization. +* "Shift+Scroll" for ten times finer zoom. +* Translations: Chinese, French, German, Russian, Turkish, Ukrainian. + +Bugs fixed: + +* Fixed broken --view options for command line thumbnail image creation. +* Some errors in Triangulation of surfaces. +* Some NURNS boolean operations that failed particularly on surfaces + created with Lathe, Revolve, or Helix. +* Segfault in Remove Spline Point context menu. +* A point in 3d constrained to any line whose length is free no longer + causes the line length to collapse. +* Curve-line constraints (in 3d), parallel constraints (in 3d), and + same orientation constraints are more robust. +* Adding some constraints (vertical, midpoint, etc) twice errors out + immediately, instead of later and in a confusing way. +* Constraining a newly placed point to a hovered entity does not cause + spurious changes in the sketch. +* Points highlighted with "Analyze → Show Degrees of Freedom" are drawn + on top of all other geometry. +* A step rotate/translate group using a group forced to triangle mesh + as a source group also gets forced to triangle mesh. +* Paste Transformed with a negative scale does not invert arcs. +* The tangent arc now modifies the original entities instead of deleting + them, such that their constraints are retained. +* When linking a sketch file, missing custom styles are now imported from + the linked file. +* 3Dconnexion SpaceMouse should now work (on Windows and macOS X). +* Improved NURBS boolean operations on curved surfaces in some cases. +* Show only usable fonts in the font selector. + +2.x +--- + +Bug fixes: + +* Do not crash when changing an unconstrained lathe group between + union and difference modes. + +2.3 +--- + +Bug fixes: + +* Do not crash when applying a symmetry constraint to two points. +* Fix TTF font metrics again (properly this time). +* Fix the "draw back faces in red" option. +* Fix export of wireframe as 3D DXF. +* Various minor crashes. + +2.2 +--- + +Other new features: + +* OS X: support 3Dconnexion devices (SpaceMouse, SpaceNavigator, etc). +* GTK: files with uppercase extensions can be opened. + +Bug fixes: + +* Do not remove autosaves after successfully opening a file, preventing + data loss in case of two abnormal terminations in a row. +* Do not crash when changing autosave interval. +* Unbreak the "Show degrees of freedom" command. +* Three.js: correctly respond to controls when browser zoom is used. +* OS X: do not completely hide main window when defocused. +* GTK: unbreak 3Dconnexion support. +* When pasting transformed entities, multiply constraint values by scale. +* Fix TTF font metrics (restore the behavior from version 2.0). +* Forcibly show the current group once we start a drawing operation. +* DXF export: always declare layers before using them. +* Do not truncate operations on selections to first 32 selected entities. +* Translate and rotate groups inherit the "suppress solid model" setting. +* DXF: files with paths containing non-ASCII or spaces can be exported + or imported. +* Significantly improved performance when dragging an entity. +* Various crashes and minor glitches. + +2.1 +--- + +New sketch features: + +* Lathe groups create circle and face entities. +* New toolbar button for creating lathe groups. +* Chord tolerance is separated into two: display chord tolerance (specified + in percents, relative to model bounding box), and export chord tolerance + (specified in millimeters as absolute value). +* Bezier spline points can be added and removed after the spline is created. +* When an unconstrained extrusion is switched between "union" and + "difference", its normal is flipped. +* Groups can be added in the middle of the stack. Note that this results + in files incompatible with version 2.0. +* Active group can be removed. +* Removing an imported group does not cause all subsequent groups to also + be removed. +* When a new group with a solid is created, the color is taken from + a previous group with a solid, if any. +* Entities in a newly active group do not become visible. +* When entities are selected, "Zoom to fit" zooms to fit only these + entities and not the entire sketch. +* Zero-length edges are reported with a "zero-length error", not + "points not all coplanar". + +New constraint features: + +* Height of the font used for drawing constraint labels can be changed. +* New constraint, length difference, placed with J. + (Patch by Peter Ruevski) +* Horizontal/vertical constraints are automatically added if a line segment + is close enough to being horizontal/vertical. This can be disabled by + holding Ctrl. +* Reference dimensions and angles can be placed with Shift+D and Shift+N. +* Copying and pasting entities duplicates any constraints that only involve + entities in the clipboard, as well as selected comments. +* Diameter constraints can be shown as radius. +* The "pi" identifier can be used in expressions. +* Constraint labels can be snapped to grid. +* Integer angles are displayed without trailing zeroes. +* Angle constraints have proper reference lines and arrowheads. +* Extension lines are drawn for point-line distance constraints. + +New solver features: + +* Sketches with redundant and unsolvable constraints are distinguished. +* New group setting, "allow redundant constraints". Note that it makes + the solver less stable. + +New rendering and styling features: + +* New line style parameter: stippling, based on ISO 128. +* Outlines of solids can be drawn in a particular style (by default, thick + lines) controlled by the "Show outline of solid model" button. +* Occluded edges can be drawn in a particular style (by default, stippled + with short dashes) controlled by the "Show hidden lines" button. +* Solids can be made transparent. + +New export/import features: + +* The old "import" command (for .slvs files) is renamed to "link". +* If a linked .slvs file is not found, first the relative path recorded + in the .slvs file is checked and then the absolute path; this is + an inversion of the previously used order. If it is still not found, + a dialog appears offering to locate it. +* DXF and DWG files can be imported, with point-coincident, horizontal and + vertical constraints automatically inferred from geometry, and distance + and angle constraints created when a dimension placed against geometry + exists. +* Triangle mesh can be exported for viewing in the browser through WebGL. +* Export dialogs remember the last file format used, and preselect it. +* Exported DXF files have exact circles, arcs and splines instead of + a piecewise linear approximation (unless hidden line removal was needed). +* Exported DXF files preserve color and line thickness. +* In exported DXF files, constraints are represented as DXF dimensions, + instead of piecewise linear geometry. +* When exporting 2d views, overlapping lines are removed. + +Other new features: + +* Native Linux (GTK 2 and GTK 3) and Mac OS X ports. +* Automatically save and then restore sketches if SolveSpace crashes. + (Patch by Marc Britten) +* Unicode is supported everywhere (filenames, group names, TTF text, + comments), although RTL scripts and scripts making heavy use of ligatures + are not rendered correctly. +* The vector font is grid-fitted when rendered on screen to make it easier + to read regardless of its size. + +2.0 +--- + +Initial public release. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..11adeb5 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,514 @@ +# cmake configuration +cmake_minimum_required(VERSION 3.18...3.25) + +cmake_policy(SET CMP0094 NEW) # for Python*_FIND_STRATEGY=LOCATION + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) + message(FATAL_ERROR + "In-tree builds are not supported; please perform an out-of-tree build:\n" + " rm -rf CMakeCache.txt CMakeFiles/\n" + " mkdir build && cd build && cmake ..") +endif() + +list(APPEND CMAKE_MODULE_PATH + "${CMAKE_SOURCE_DIR}/cmake/") + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED YES) + +# for /MT on MSVC +set(CMAKE_USER_MAKE_RULES_OVERRIDE + "${CMAKE_SOURCE_DIR}/cmake/c_flag_overrides.cmake") +set(CMAKE_USER_MAKE_RULES_OVERRIDE_CXX + "${CMAKE_SOURCE_DIR}/cmake/cxx_flag_overrides.cmake") + +if(CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") +endif() + +if (APPLE) + # Docs say this must be set before the first project() call + set(CMAKE_OSX_DEPLOYMENT_TARGET "10.12" CACHE STRING "macOS minimum supported version") +endif() + +# project + +# NOTE TO PACKAGERS: The embedded git commit hash is critical for rapid bug triage when the builds +# can come from a variety of sources. If you are mirroring the sources or otherwise build when +# the .git directory is not present, please comment the following line: +include(GetGitCommitHash) +# and instead uncomment the following, adding the complete git hash of the checkout you are using: +# set(GIT_COMMIT_HASH 0000000000000000000000000000000000000000) + +string(SUBSTRING "${GIT_COMMIT_HASH}" 0 8 solvespace_GIT_HASH) +project(solvespace + VERSION 3.2 + LANGUAGES C CXX ASM) + +if(EMSCRIPTEN) + # Enable optimization. Workaround for "too many locals" error when runs in the browser. + if(NOT CMAKE_BUILD_TYPE OR CMAKE_BUILD_TYPE STREQUAL "Debug") + add_compile_options(-O1) + endif() + + set(INCLUDE_NATIVE_COMPONENTS OFF) +else() + set(INCLUDE_NATIVE_COMPONENTS ON) +endif() + +set(ENABLE_GUI ON CACHE BOOL + "Whether the graphical interface is enabled") +set(USE_QT_GUI OFF CACHE BOOL + "Whether the Qt library used to build graphical interface") +set(ENABLE_CLI ${INCLUDE_NATIVE_COMPONENTS} CACHE BOOL + "Whether the command line interface is enabled") +set(ENABLE_TESTS ${INCLUDE_NATIVE_COMPONENTS} CACHE BOOL + "Whether the test suite will be built and run") +set(ENABLE_COVERAGE OFF CACHE BOOL + "Whether code coverage information will be collected") +set(ENABLE_SANITIZERS OFF CACHE BOOL + "Whether to enable Clang's AddressSanitizer and UndefinedBehaviorSanitizer") +set(ENABLE_OPENMP OFF CACHE BOOL + "Whether geometric operations will be parallelized using OpenMP") +set(ENABLE_LTO OFF CACHE BOOL + "Whether interprocedural (global) optimizations are enabled") +option(FORCE_VENDORED_Eigen3 + "Whether we should use our bundled Eigen even in the presence of a system copy" + OFF) + +set(OPENGL 3 CACHE STRING "OpenGL version to use (one of: 1 3)") + +set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin) +set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin) +if("${CMAKE_GENERATOR}" STREQUAL "Xcode") + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY $<1:${CMAKE_BINARY_DIR}/bin>) +endif() + +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 5.0) + # GCC 4.8/4.9 ship with broken but present . meh. + message(FATAL_ERROR "GCC 5.0+ is required") + endif() +endif() + +# common compiler flags +include(CheckCXXCompilerFlag) + +if (NOT APPLE) + set(FILE_PREFIX_MAP "-ffile-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=.") + check_cxx_compiler_flag("${FILE_PREFIX_MAP}" HAS_FILE_PREFIX_MAP) + if(HAS_FILE_PREFIX_MAP) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${FILE_PREFIX_MAP}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${FILE_PREFIX_MAP}") + endif() +endif() + +if(MINGW) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -static-libgcc") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static-libgcc -static-libstdc++") + if(CMAKE_SIZEOF_VOID_P EQUAL 4) + # Link 32 bit SolveSpace with --large-address-aware which allows it to access + # up to 3GB on a properly configured 32 bit Windows and up to 4GB on 64 bit. + # See https://msdn.microsoft.com/en-us/library/aa366778 + set(CMAKE_EXE_LINKER_FLAGS "-Wl,--large-address-aware") + endif() +endif() + +# Ensure that all platforms use 64-bit IEEE floating point operations for consistency; +# this is most important for the testsuite, which compares savefiles directly +# and depends on consistent rounding of intermediate results. +if(CMAKE_SYSTEM_PROCESSOR STREQUAL "i686" OR CMAKE_SYSTEM_PROCESSOR STREQUAL "X86") + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + set(FLOAT_FLAGS "-mfpmath=sse -msse2") + elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set(FLOAT_FLAGS "-msse2") + endif() + + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${FLOAT_FLAGS}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${FLOAT_FLAGS}") +endif() + +if(ENABLE_LTO) + include(CheckIPOSupported) + check_ipo_supported() + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) +endif() + +if(ENABLE_OPENMP) + find_package( OpenMP REQUIRED ) + if(OPENMP_FOUND) + add_library(slvs_openmp INTERFACE) + target_compile_options(slvs_openmp INTERFACE "SHELL:${OpenMP_CXX_FLAGS}") + target_link_libraries(slvs_openmp INTERFACE + ${OpenMP_CXX_LIBRARIES}) + target_include_directories(slvs_openmp SYSTEM INTERFACE + ${OpenMP_CXX_INCLUDE_DIRS}) + message(STATUS "found OpenMP, compiling with flags: " ${OpenMP_CXX_FLAGS} ) + endif() +endif() + +if(CMAKE_SYSTEM_NAME STREQUAL "Linux" OR CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") + set(CMAKE_EXE_LINKER_FLAGS "-Wl,--as-needed ${CMAKE_EXE_LINKER_FLAGS}") +endif() + +if(ENABLE_SANITIZERS) + if(NOT SANITIZERS) + if(MSVC) + if(MSVC_VERSION LESS 1928) + # Technically 1928 is also used for VS 16.8, but CMake doesn't have a way + # to distinguish patch versions of MSVC, so this is good enough + message(FATAL_ERROR "Using sanitizers with MSVC requires Visual Studio 16.9 or newer") + endif() + set(SANITIZERS "address") + else() + set(SANITIZERS "address;undefined") + endif() + endif() + + foreach(option ${SANITIZERS}) + if(option STREQUAL "thread" AND NOT MSVC) + list(APPEND SANITIZE_OPTIONS thread) + elseif(option STREQUAL "address") + list(APPEND SANITIZE_OPTIONS address) + elseif(option STREQUAL "undefined" AND NOT MSVC) + list(APPEND SANITIZE_OPTIONS alignment bounds) + list(APPEND SANITIZE_OPTIONS shift signed-integer-overflow integer-divide-by-zero) + list(APPEND SANITIZE_OPTIONS null bool enum) + list(APPEND SANITIZE_OPTIONS return) + else() + message(FATAL_ERROR "Unknown sanitizer ${option}") + endif() + endforeach() + + message(STATUS "Using sanitizer options ${SANITIZE_OPTIONS}") + + if(MSVC) + string(REPLACE ";" " /fsanitize=" SANITIZE_OPTIONS "${SANITIZE_OPTIONS}") + set(SANITIZE_FLAGS "/Zi /fsanitize=${SANITIZE_OPTIONS}") + else() + string(REPLACE ";" "," SANITIZE_OPTIONS "${SANITIZE_OPTIONS}") + set(SANITIZE_FLAGS "-O1 -fsanitize=${SANITIZE_OPTIONS}") + if (NOT APPLE) + set(SANITIZE_FLAGS "${SANITIZE_FLAGS} -fno-sanitize-recover=address,undefined") + endif() + endif() + + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set(SANITIZE_FLAGS "${SANITIZE_FLAGS} -fno-omit-frame-pointer -fno-optimize-sibling-calls") + elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + set(SANITIZE_FLAGS "${SANITIZE_FLAGS} -fuse-ld=gold") + elseif(NOT MSVC) + message(FATAL_ERROR "Sanitizers are only available when using MSVC, GCC, or Clang") + endif() + + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${SANITIZE_FLAGS}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${SANITIZE_FLAGS}") +endif() + +# common dependencies + +if(APPLE) + set(CMAKE_FIND_FRAMEWORK FIRST) +endif() + +if(ENABLE_GUI OR ENABLE_CLI) + message(STATUS "Using in-tree libdxfrw") + add_subdirectory(extlib/libdxfrw) +endif() + +message(STATUS "Using in-tree mimalloc") +set(MI_OVERRIDE OFF CACHE BOOL "") +set(MI_BUILD_SHARED OFF CACHE BOOL "") +set(MI_BUILD_OBJECT OFF CACHE BOOL "") +set(MI_BUILD_TESTS OFF CACHE BOOL "") +if(EMSCRIPTEN) + # HACK: We only use mimalloc for the temporary heap, not as a general purpose + # allocator. However, the Emscripten port of mimalloc assumes that it is used + # as the system allocator, and depends on the slow emmalloc allocator. + # We do not want to enable emmalloc, as that would slow down allocations all + # over the place, so we must either use mimalloc as the system allocator as + # well, or override its definitions to just use the existing system allocator. + # The latter option is the least intrusive change from a stability standpoint, + # so for now that's what we do here. We can try to convert to mimalloc wholsale + # and remove this hack if we find the need to do so in the future. + set(MI_EXTRA_CPPDEFS "emmalloc_free=free;emmalloc_memalign=memalign" CACHE INTERNAL "") +endif() +add_subdirectory(extlib/mimalloc EXCLUDE_FROM_ALL) +set(MIMALLOC_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/extlib/mimalloc/include) + +if(NOT FORCE_VENDORED_Eigen3) + find_package(Eigen3 CONFIG) +endif() +if(FORCE_VENDORED_Eigen3 OR NOT EIGEN3_INCLUDE_DIRS) + message(STATUS "Using in-tree Eigen") + set(EIGEN3_FOUND YES) + set(EIGEN3_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/eigen) +else() + message(STATUS "Using system Eigen: ${EIGEN3_INCLUDE_DIRS}") +endif() +if(NOT EXISTS "${EIGEN3_INCLUDE_DIRS}") + message(FATAL_ERROR "Eigen 3 not found on system or in-tree") +endif() + + +if(ENABLE_GUI OR ENABLE_CLI) + if(WIN32 OR APPLE OR EMSCRIPTEN) + # On Win32 and macOS we use vendored packages, since there is little to no benefit + # to trying to find system versions. In particular, trying to link to libraries from + # Homebrew or macOS system libraries into the .app file is highly likely to result + # in incompatibilities after upgrades. + + include(FindVendoredPackage) + include(AddVendoredSubdirectory) + + set(FORCE_VENDORED_ZLIB ON) + set(FORCE_VENDORED_PNG ON) + set(FORCE_VENDORED_Freetype ON) + + # HACK: This is essentially what `find_vendored_package()` does, except we also + # define the ZLIB::ZLIB target to be an alias for zlibstatic before calling + # `find_package()` in order to avoid having it defined as an imported target + # by the built-in FindZLIB.cmake module, which breaks libpng's png_static + # target that relies on ZLIB::ZLIB instead of on zlibstatic. + # Once zlib releases its develop branch post 1.3.1, the built-in module will not + # generate an imported target since we'll have proper targets, but libpng would + # still need to be updated to use ZLIB::ZLIBSTATIC instead of ZLIB::ZLIB, so this + # ugly hack will have to stay until both libraries release fixed versions. + add_vendored_subdirectory(extlib/zlib) + set(ZLIB_LIBRARY zlibstatic CACHE INTERNAL "") + set(ZLIB_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/extlib/zlib CACHE INTERNAL "") + add_library(ZLIB::ZLIB ALIAS ${ZLIB_LIBRARY}) + find_package(ZLIB REQUIRED) + set(ZLIB_FOUND ${ZLIB_FOUND} CACHE INTERNAL "") + + list(APPEND ZLIB_INCLUDE_DIR ${CMAKE_BINARY_DIR}/extlib/zlib) + + # There's an issue with the zlib configuration, which doesn't provide a way to + # disable the shared or static library builds, and on UNIX sets the base name of + # the output for both the static and shared targets to `libz`. As Emscripten doesn't + # support shared libraries, CMake converts the shared library target to a static + # library and both targets end up with the name `libz.a`, causing Ninja to complain + # about different rules generating the same target. As a workaround, force the output + # name of the static library target to be different on Emscripten. + # NOTE: The unreleased develop branch of zlib after version 1.3.1 allows disabling + # the shared library build, which is the proper fix for this issue, so when the + # next version of zlib is released and updated as a dependency, `ZLIB_BUILD_SHARED` + # should be set to `OFF` instead of this hack. + if(EMSCRIPTEN AND UNIX) + set_target_properties(${ZLIB_LIBRARY} PROPERTIES OUTPUT_NAME zlibstatic) + endif() + + find_vendored_package(PNG libpng + SKIP_INSTALL_ALL ON + PNG_LIBRARY png_static + PNG_SHARED OFF + PNG_STATIC ON + PNG_TOOLS OFF + PNG_TESTS OFF + PNG_FRAMEWORK OFF + PNG_PNG_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/extlib/libpng) + list(APPEND PNG_PNG_INCLUDE_DIR ${CMAKE_BINARY_DIR}/extlib/libpng) + + find_vendored_package(Freetype freetype + FT_DISABLE_BZIP2 ON + FT_DISABLE_HARFBUZZ ON + FT_DISABLE_BROTLI ON + FT_DISABLE_PNG ON + FREETYPE_LIBRARY freetype + FREETYPE_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/freetype/include) + + message(STATUS "Using in-tree pixman") + set(PIXMAN_FOUND YES) + set(PIXMAN_LIBRARY pixman) + set(PIXMAN_BUILD_TESTS OFF CACHE BOOL "") + set(PIXMAN_BUILD_DEMOS OFF CACHE BOOL "") + + set(PIXMAN_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/pixman/pixman) + list(APPEND PIXMAN_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/pixman/pixman) + add_vendored_subdirectory(extlib/pixman) + + message(STATUS "Using in-tree cairo") + add_vendored_subdirectory(extlib/cairo) + set(CAIRO_FOUND YES) + set(CAIRO_LIBRARIES cairo) + set(CAIRO_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/cairo/src) + list(APPEND CAIRO_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/cairo/src) + else() + # On Linux and BSDs we're a good citizen and link to system libraries. + find_package(Backtrace) + find_package(PkgConfig REQUIRED) + find_package(ZLIB REQUIRED) + find_package(PNG REQUIRED) + find_package(Freetype REQUIRED) + if (NOT USE_QT_GUI OR ENABLE_CLI OR ENABLE_TESTS) + find_package(Cairo REQUIRED) + endif() + endif() +endif() + +# GUI dependencies + +if(ENABLE_GUI) + if(WIN32) + if(OPENGL STREQUAL "3") + message(STATUS "Using in-tree ANGLE") + set(ANGLE_STATIC ON CACHE INTERNAL "") + set(ANGLE_ENABLE_D3D9 ON CACHE INTERNAL "") + set(ANGLE_ENABLE_D3D11 ON CACHE INTERNAL "") + set(ANGLE_ENABLE_OPENGL ON CACHE INTERNAL "") + set(ANGLE_ENABLE_ESSL ON CACHE INTERNAL "") + set(ANGLE_ENABLE_GLSL ON CACHE INTERNAL "") + set(ANGLE_ENABLE_HLSL ON CACHE INTERNAL "") + add_vendored_subdirectory(extlib/angle) + set(OPENGL_LIBRARIES EGL GLESv2) + set(OPENGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/extlib/angle/include) + else() + find_package(OpenGL REQUIRED) + endif() + + if(MSVC AND ${CMAKE_SIZEOF_VOID_P} EQUAL 4) + message(STATUS "Using prebuilt SpaceWare") + set(SPACEWARE_FOUND TRUE) + set(SPACEWARE_INCLUDE_DIR + "${CMAKE_SOURCE_DIR}/extlib/si") + set(SPACEWARE_LIBRARIES + "${CMAKE_SOURCE_DIR}/extlib/si/siapp.lib") + endif() + elseif(APPLE) + find_package(OpenGL REQUIRED) + find_library(APPKIT_LIBRARY AppKit REQUIRED) + elseif(EMSCRIPTEN) + # Everything is built in + else() + find_package(OpenGL REQUIRED) + find_package(SpaceWare) + pkg_check_modules(FONTCONFIG REQUIRED fontconfig) + if (NOT USE_QT_GUI) + pkg_check_modules(JSONC REQUIRED json-c) + pkg_check_modules(GTKMM REQUIRED gtkmm-3.0>=3.18 pangomm-1.4 x11) + endif() + endif() +endif() + +# code coverage + +if(ENABLE_COVERAGE) + if(CMAKE_CXX_COMPILER_ID STREQUAL GNU) + find_program(GCOV gcov) + elseif(CMAKE_CXX_COMPILER_ID MATCHES Clang) + find_program(LLVM_COV llvm-cov) + + if(LLVM_COV) + set(GCOV ${CMAKE_CURRENT_BINARY_DIR}/llvm-gcov.sh) + file(WRITE ${GCOV} "#!/bin/sh -e\n${LLVM_COV} gcov $*") + execute_process(COMMAND chmod +x ${GCOV}) + endif() + endif() + + find_program(LCOV lcov) + find_program(GENHTML genhtml) + if(NOT GCOV OR NOT LCOV OR NOT GENHTML) + message(FATAL_ERROR "gcov/llvm-cov and lcov are required for producing coverage reports") + endif() +endif() + +# translations + +find_program(XGETTEXT xgettext) +find_program(MSGINIT msginit) +find_program(MSGMERGE msgmerge) +if(XGETTEXT AND MSGINIT AND MSGMERGE) + set(HAVE_GETTEXT TRUE) +else() + message(WARNING "Gettext not found, translations will not be updated") + set(HAVE_GETTEXT FALSE) +endif() + +# solvespace-only compiler flags + +if(WIN32) + add_definitions( + -D_CRT_SECURE_NO_DEPRECATE + -D_CRT_SECURE_NO_WARNINGS + -D_SCL_SECURE_NO_WARNINGS + -DWINVER=0x0501 + -D_WIN32_WINNT=0x0501 + -D_WIN32_IE=_WIN32_WINNT + -DISOLATION_AWARE_ENABLED + -DWIN32 + -DWIN32_LEAN_AND_MEAN + -DUNICODE + -D_UNICODE + -DNOMINMAX + -D_USE_MATH_DEFINES) +endif() + +if(MSVC) + # Many versions of MSVC do not have the (C99) inline keyword, instead + # they have their own __inline; this breaks `static inline` functions. + # We do not want to care and so we fix this with a definition. + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /Dinline=__inline") + # Same for the (C99) __func__ special variable; we use it only in C++ code. + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /D__func__=__FUNCTION__") + + # Multi-processor Compilation + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") + + # We rely on these /we flags. They correspond to the GNU-style flags below as + # follows: /w4062=-Wswitch + set(WARNING_FLAGS "${WARNING_FLAGS} /we4062") + + # Link 32 bit SolveSpace with /LARGEADDRESSAWARE which allows it to access + # up to 3GB on a properly configured 32 bit Windows and up to 4GB on 64 bit. + # See https://msdn.microsoft.com/en-us/library/aa366778 + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /LARGEADDRESSAWARE") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /LARGEADDRESSAWARE") +endif() + +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set(WARNING_FLAGS "-Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers") + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set(WARNING_FLAGS "${WARNING_FLAGS} -Wfloat-conversion") + endif() + # We rely on these -Werror flags. + set(WARNING_FLAGS "${WARNING_FLAGS} -Werror=switch") +endif() + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WARNING_FLAGS}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WARNING_FLAGS}") + +if(WIN32) + set(CMAKE_RC_FLAGS "${CMAKE_RC_FLAGS} -l0") +endif() + +if(ENABLE_COVERAGE) + if(NOT (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR + CMAKE_CXX_COMPILER_ID MATCHES "Clang")) + message(FATAL_ERROR "Code coverage is only available on GCC and Clang") + endif() + + if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + message(FATAL_ERROR "Code coverage produces reliable results only on Debug builds") + endif() + + # With -fexceptions, every call becomes a branch. While technically accurate, + # this is not useful for us. + set(COVERAGE_FLAGS -fno-exceptions --coverage) + set(COVERAGE_LIBRARY --coverage) +endif() + +# application components +if(ENABLE_GUI OR ENABLE_CLI) + add_subdirectory(res) + add_subdirectory(exposed) +endif() +add_subdirectory(src) +if(ENABLE_TESTS) + add_subdirectory(test) +endif() +if((ENABLE_CLI OR ENABLE_GUI) AND (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")) + add_subdirectory(bench) +else() + message(STATUS "Benchmarking disabled in debug and library builds.") +endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..27f90fd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,291 @@ +Contributing to SolveSpace +========================== + +Contributing bug reports +------------------------ + +Bug reports are always welcome! When reporting a bug, please include the following: + + * The version of SolveSpace (use Help → About...); + * The operating system; + * The save file that reproduces the incorrect behavior, or, if trivial or impossible, + instructions for reproducing it. + +GitHub does not allow attaching `*.slvs` files, but it does allow attaching `*.zip` files, +so any savefiles should first be archived. + +Licensing +--------------- + +SolveSpace is licensed under the GPLv3 or later and any contributions +must be made available under the terms of that license. + +Contributing translations +------------------------- + +To contribute a translation, not a lot is necessary—at a minimum, you need to be able +to edit .po files with a tool such as [poedit](https://poedit.net/). Once you have +such a tool installed, take `res/messages.pot` and start translating! + +However, if you want to see your translation in action, a little more work is necessary. +First, you need to be able to build SolveSpace; see [README](README.md). After that: + + * Copy `res/messages.pot` to `res/locales/xx_YY.po`, where `xx` is an ISO 639-1 + country code, and `YY` is an ISO 3166-1 language code. + * Add a line `xx-YY,LCID,Name` to `res/locales.txt`, where `xx-YY` have the same + meaning as above, `LCID` is a Windows Language Code Identifier ([MS-LCID][] + has a complete list), and `Name` is the full name of your locale in your language. + * Add `locales/xx_YY.po` in `res/CMakeLists.txt`—search for `locales/en_US.po` + to see where it should be added. + +You're done! Recompile SolveSpace and you should be able to select your translation +via Help → Language. + +[MS-LCID]: https://msdn.microsoft.com/en-us/library/cc233965.aspx + +Contributing code +----------------- + +SolveSpace is written in C++, and currently targets all compilers compliant with C++11. +This includes GCC 5 and later, Clang 3.3 and later, and Visual Studio 12 (2013) and later. + +### High-level conventions + +#### Portability + +SolveSpace aims to consist of two general parts: a fully portable core, and platform-specific +UI and support code. Anything outside of `src/platform/` should only use standard C++11, +and rely on `src/platform/platform.h` to interact with the OS where this cannot be done +through the C++11 standard library. + +#### Libraries + +SolveSpace primarily relies on the C++11 STL. STL has well-known drawbacks, but is also +widely supported, used, and understood. SolveSpace also includes a fair amount of use of +bespoke containers List and IdList; these provide STL iterators, and can be used when +convenient, such as when reusing other code. + +One notable departure here is the STL I/O streams. SolveSpace does not use STL I/O streams +for two reasons: (i) the interface is borderline unusable, and (ii) on Windows it is not +possible to open files with Unicode paths through STL. + +When using external libraries (other than to access platform features), the libraries +should satisfy the following conditions: + + * Portable, and preferably not interacting with the platform at all; + * Can be included as a CMake subproject, to facilitate Windows, Android, etc. builds; + * Use a license less restrictive than GPL (BSD/MIT, Apache2, MPL, etc.) + +#### String encoding + +Internally, SolveSpace exclusively stores and uses UTF-8 for all purposes; any `std::string` +may be assumed to be encoded in UTF-8. On Windows, UTF-8 strings are converted to and from +wide strings at the boundary; see [UTF-8 Everywhere][utf8] for details. + +[utf8]: http://utf8everywhere.org/ + +#### String formatting + +For string formatting, a wrapper around `sprintf`, `ssprintf`, is used. A notable +pitfall when using it is trying to pass an `std::string` argument without first converting +it to a C string with `.c_str()`. + +#### Filesystem access + +For filesystem access, the C standard library is used. The `ssfopen` and `ssremove` +wrappers are provided that accept UTF-8 encoded paths. + +#### Assertions + +To ensure that internal invariants hold, the `ssassert` function is used, e.g. +`ssassert(!isFoo, "Unexpected foo condition");`. Unlike the standard `assert` function, +the `ssassert` function is always enabled, even in release builds. It is more valuable +to discover a bug through a crash than to silently generate incorrect results, and crashes +do not result in losing more than a few minutes of work thanks to the autosave feature. + +### Use of C++ features + +The conventions described in this section should be used for all new code, but there is a lot +of existing code in SolveSpace that does not use them. This is fine; don't touch it if it works, +but if you need to modify it anyway, might as well modernize it. + +#### Exceptions + +Exceptions are not used primarily because SolveSpace's testsuite uses measurement +of branch coverage, important for the critical parts such as the geometric kernel. +Every function call with exceptions enabled introduces a branch, making branch coverage +measurement useless. + +#### Operator overloading + +Operator overloading is not used primarily for historical reasons. Instead, method such +as `Plus` are used. + +#### Member visibility + +Member visibility is not used for implementation hiding. Every member field and function +is `public`. + +#### Constructors + +Constructors are not used for initialization, chiefly because indicating an error +in a constructor would require throwing an exception, nor does it use constructors for +blanket zero-initialization because of the performance impact of doing this for common +POD classes like `Vector`. + +Instances can be zero-initialized using the aggregate-initialization syntax, e.g. `Foo foo = {};`. +This zero-initializes the POD members and default-initializes the non-POD members, generally +being an equivalent of `memset(&foo, 0, sizeof(foo));` but compatible with STL containers. + +#### Input- and output-arguments + +Functions accepting an input argument take it either by-value (`Vector v`) or +by-const-reference (`const Vector &v`). Generally, passing by-value is safer as the value +cannot be aliased by something else, but passing by-const-reference is faster, as a copy is +eliminated. Small values should always be passed by-value, and otherwise functions that do not +capture pointers into their arguments should take them by-const-reference. Use your judgement. + +Functions accepting an output argument always take it by-pointer (`Vector *v`). This makes +it immediately visible at the call site as it is seen that the address is taken. Arguments +are never passed by-reference, except when needed for interoperability with STL, etc. + +#### Iteration + +`foreach`-style iteration is preferred for both STL and `List`/`IdList` containers as it indicates +intent clearly, as opposed to `for`-style. + +#### Const correctness + +Functions that do not mutate `this` should be marked as `const`; when iterating a collection +without mutating any of its elements, `for(const Foo &elem : collection)` is preferred to indicate +the intent. + +### Coding style + +Code is formatted by the following rules: + + * Code is indented using 4 spaces, with no trailing spaces, and lines are wrapped + at 100 columns; + * Braces are placed at the end of the line with the declaration or control flow statement; + * Braces are used with every control flow statement, even if there is only one statement + in the body; + * There is no space after control flow keywords (`if`, `while`, etc.); + * Identifiers are formatted in camel case; variables start with a lowercase letter + (`exampleVariable`) and functions start with an uppercase letter (`ExampleFunction`). + +For example: + +```c++ +std::string SolveSpace::Dirname(std::string filename) { + int slash = filename.rfind(PATH_SEP); + if(slash >= 0) { + return filename.substr(0, slash); + } + + return ""; +} +``` + +If you install [clang-format][], this style can be automatically applied by staging your changes +with `git add -u`, running `git clang-format`, and staging any changes it made again. + +[clang-format]: https://clang.llvm.org/docs/ClangFormat.html + +Debugging code +-------------- + +SolveSpace releases are thoroughly tested but sometimes they contain crash +bugs anyway. The reason for such crashes can be determined only if the executable +was built with debug information. + +### Debugging a released version + +The Linux distributions usually include separate debug information packages. +On a Debian derivative (e.g. Ubuntu), these can be installed with: + + apt-get install solvespace-dbg + +The macOS releases include the debug information, and no further action +is needed. + +The Windows releases include the debug information on the GitHub +[release downloads page](https://github.com/solvespace/solvespace/releases). + +### Debugging a custom build + +If you are building SolveSpace yourself on macOS, use the XCode +CMake generator, then open the project in XCode as usual, select +the Debug build scheme, and build the project: + + cd build + cmake .. -G Xcode [other cmake args...] + +If you are building SolveSpace yourself on any Unix-like platform, +configure or re-configure SolveSpace to produce a debug build, and +then build it: + + cd build + cmake .. -DCMAKE_BUILD_TYPE=Debug [other cmake args...] + make + +If you are building SolveSpace yourself using the Visual Studio IDE, +select Debug from the Solution Configurations list box on the toolbar, +and build the solution. + +### Debugging with gdb + +gdb is a debugger that is mostly used on Linux. First, run SolveSpace +under debugging: + + gdb [path to solvespace executable] + (gdb) run + +Then, reproduce the crash. After the crash, attach the output in +the console, as well as output of the following gdb commands to +a bug report: + + (gdb) backtrace + (gdb) info locals + +If the crash is not easy to reproduce, please generate a core file, +which you can use to resume the debugging session later, and provide +any other information that is requested: + + (gdb) generate-core-file + +This will generate a large file called like `core.1234` in the current +directory; it can be later re-loaded using `gdb --core core.1234`. + +### Debugging with lldb + +lldb is a debugger that is mostly used on macOS. First, run SolveSpace +under debugging: + + lldb [path to solvespace executable] + (lldb) run + +Then, reproduce the crash. After the crash, attach the output in +the console, as well as output of the following gdb commands to +a bug report: + + (lldb) backtrace all + (lldb) frame variable + +If the crash is not easy to reproduce, please generate a core file, +which you can use to resume the debugging session later, and provide +any other information that is requested: + + (lldb) process save-core "core" + +This will generate a large file called `core` in the current +directory; it can be later re-loaded using `lldb -c core`. + +### Debugging GUI-related bugs on Linux + +There are several environment variables available that make crashes +earlier and errors more informative. Before running SolveSpace, run +the following commands in your shell: + + export G_DEBUG=fatal_warnings + export LIBGL_DEBUG=1 + export MESA_DEBUG=1 diff --git a/COPYING.txt b/COPYING.txt new file mode 100644 index 0000000..a737dcf --- /dev/null +++ b/COPYING.txt @@ -0,0 +1,675 @@ + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a4513b9 --- /dev/null +++ b/README.md @@ -0,0 +1,356 @@ +# SolveSpace + +SolveSpace Logo + +[![Build Status](https://github.com/solvespace/solvespace/workflows/CD/badge.svg)](https://github.com/solvespace/solvespace/actions) +[![solvespace](https://snapcraft.io/solvespace/badge.svg)](https://snapcraft.io/solvespace) +[![solvespace](https://snapcraft.io/solvespace/trending.svg?name=0)](https://snapcraft.io/solvespace) + +This repository contains the source code of [SolveSpace][], a parametric +2d/3d CAD tool. + +[solvespace]: https://solvespace.com + +## Community + +The official SolveSpace [website][sswebsite] has [tutorials][sstutorial], +[reference manual][ssref] and a [forum][ssforum]; there is also an official +IRC channel [#solvespace at web.libera.chat][ssirc]. + +[sswebsite]: http://solvespace.com/ +[ssref]: http://solvespace.com/ref.pl +[sstutorial]: http://solvespace.com/tutorial.pl +[ssforum]: http://solvespace.com/forum.pl +[ssirc]: https://web.libera.chat/#solvespace + +# Installation + +### Via Official Packages + +_Official_ release packages for macOS (>=10.6 64-bit) and Windows +(>=Vista 32-bit) are available via [GitHub releases][rel]. These packages are +automatically built by the SolveSpace maintainers for each stable release. + +[rel]: https://github.com/solvespace/solvespace/releases + +### Via Flathub + +Official releases can be installed as a Flatpak from Flathub. + +[Get SolveSpace from Flathub](https://flathub.org/apps/details/com.solvespace.SolveSpace) + +These should work on any Linux distribution that supports Flatpak. + +### Via Snap Store + +Official releases can be installed from the `stable` channel. + +Builds from master are automatically released to the `edge` channel in the Snap +Store. Those packages contain the latest improvements, but receive less testing +than release builds. + +[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/solvespace) + +Or install from a terminal: + +```sh +# for the latest stable release: +snap install solvespace + +# for the bleeding edge builds from master: +snap install solvespace --edge +``` + +### Via automated edge builds + +> :warning: **Edge builds might be unstable or contain severe bugs!** +> They are intended for experienced users to test new features or verify bugfixes. + +Cutting edge builds from the latest master commit are available as zip archives +from the following links: + +- [macOS](https://nightly.link/solvespace/solvespace/workflows/cd/master/macos.zip) +- [Windows with OpenMP enabled 32bit](https://nightly.link/solvespace/solvespace/workflows/cd/master/windows_x86.zip) +- [Windows 32bit](https://nightly.link/solvespace/solvespace/workflows/cd/master/windows_single_core_x86.zip) +- [Windows with OpenMP enabled 64bit](https://nightly.link/solvespace/solvespace/workflows/cd/master/windows_x64.zip) +- [Windows 64bit](https://nightly.link/solvespace/solvespace/workflows/cd/master/windows_single_core_x64.zip) + +**Please note that the 64bit Windows versions do *not* support 6DOF (SpeceMouse, SpaceNavigator) controllers.** + +Extract the downloaded archive and install or execute the contained file as is +appropriate for your platform. + +### Via source code + +Irrespective of the OS used, before building, check out the project and the +necessary submodules: + +```sh +git clone https://github.com/solvespace/solvespace +cd solvespace +git submodule update --init +``` + +You will need `git`. See the platform specific instructions below to install it. + +## Building on Linux + +### Building for Linux + +You will need the usual build tools, CMake, zlib, libpng, cairo, freetype. To +build the GUI, you will need fontconfig, gtkmm 3.0 (version 3.16 or later) for +GTK, or QT6 for the newer QT interface, pangomm 1.4, OpenGL and OpenGL GLU, +and optionally, the Space Navigator client library. + +On a Debian derivative (e.g. Ubuntu) these can be installed with: + +```sh +sudo apt install git build-essential cmake zlib1g-dev libpng-dev \ + libcairo2-dev libfreetype6-dev libjson-c-dev \ + libfontconfig1-dev libpangomm-1.4-dev libgl-dev \ + libglu-dev libspnav-dev libgtkmm-3.0-dev qt6-base-dev +``` + +On a RedHat derivative (e.g. Fedora) the dependencies can be installed with: + +```sh +sudo dnf install git gcc-c++ cmake zlib-devel libpng-devel \ + cairo-devel freetype-devel json-c-devel \ + fontconfig-devel pangomm-devel mesa-libGL-devel \ + mesa-libGLU-devel libspnav-devel gtkmm30-devel \ + qt6-qtbase-devel +``` +`gtkmm30-devel` is required to build the GTK version and `qt6-qtbase-devel` is required to build the QT version. One or +the other may be omitted if both versions are not needed. Likewise with `libgtkmm-3.0-dev` and `qt6-base-dev` for Debuntu +respectively. + +Before building, [check out the project and the necessary submodules](#via-source-code). + +After that, build SolveSpace as following: + +```sh +mkdir build +cd build +cmake .. -DCMAKE_BUILD_TYPE=Release -DENABLE_OPENMP=ON [-DENABLE_LTO=ON] [-DUSE_QT_GUI=ON] [-DENABLE_GUI=OFF] +make + +# Optionally +sudo make install +``` +Optional: + - -DENABLE_LTO=ON: Enable Link Time Optimization at the expense of longer build time. + - -DUSE_QT_GUI=ON: Build the newer QT GUI interface. + - -DENABLE_GUI=OFF: Build only the command-line interface + +The GTK graphical interface is built as `build/bin/solvespace`, and the command-line +interface is built as `build/bin/solvespace-cli`. The QT graphical interface is built +as `build/bin/solvespace-qt`. + +### Building for Windows + +Ubuntu will require 20.04 or above. Cross-compiling with WSL is also confirmed +to work. + +You will need the usual build tools, CMake, and a Windows cross-compiler. On a +Debian derivative (e.g. Ubuntu) these can be installed with: + +```sh +apt-get install git build-essential cmake mingw-w64 +``` + +Before building, [check out the project and the necessary submodules](#via-source-code). + +Build 64-bit SolveSpace with the following: + +```sh +mkdir build +cd build +cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw64.cmake \ + -DCMAKE_BUILD_TYPE=Release +make +``` + +The graphical interface is built as `build/bin/solvespace.exe`, and the +command-line interface is built as `build/bin/solvespace-cli.exe`. + +Space Navigator support will not be available. + +### Building for web (very experimental) + +**Please note that this port contains many critical bugs and unimplemented core functions.** + +You will need the usual build tools, cmake and [Emscripten][]. On a Debian derivative (e.g. Ubuntu) dependencies other than Emscripten can be installed with: + +```sh +apt-get install git build-essential cmake +``` + +First, install and prepare `emsdk`: + +```sh +git clone https://github.com/emscripten-core/emsdk +cd emsdk +./emsdk install latest +./emsdk activate latest +source ./emsdk_env.sh +cd .. +``` + +Before building, [check out the project and the necessary submodules](#via-source-code). + +After that, build SolveSpace as following: + +```sh +mkdir build +cd build +emcmake cmake .. -DCMAKE_BUILD_TYPE=Release -DENABLE_LTO="ON" -DENABLE_TESTS="OFF" -DENABLE_CLI="OFF" -DENABLE_COVERAGE="OFF" +make +``` + +The graphical interface is built as multiple files in the `build/bin` directory with names +starting with `solvespace`. It can be run locally with `emrun build/bin/solvespace.html`. + +The command-line interface is not available. + +[emscripten]: https://emscripten.org/ + +## Building on macOS + +You will need git, XCode tools, CMake and libomp. Git, CMake and libomp can be installed +via [Homebrew][]: + +```sh +brew install git cmake libomp +``` + +XCode has to be installed via AppStore or [the Apple website][appledeveloper]; +it requires a free Apple ID. + +Before building, [check out the project and the necessary submodules](#via-source-code). + +After that, build SolveSpace as following: + +```sh +mkdir build +cd build +cmake .. -DCMAKE_BUILD_TYPE=Release -DENABLE_OPENMP=ON +make +``` + +Link Time Optimization is supported by adding `-DENABLE_LTO=ON` to cmake at the +expense of longer build time. + +Alternatively, generate an XCode project, open it, and build the "Release" scheme: + +```sh +mkdir build +cd build +cmake .. -G Xcode +``` + +The application is built in `build/bin/SolveSpace.app`, the graphical interface executable +is `build/bin/SolveSpace.app/Contents/MacOS/SolveSpace`, and the command-line interface executable +is `build/bin/SolveSpace.app/Contents/MacOS/solvespace-cli`. + +[homebrew]: https://brew.sh/ +[appledeveloper]: https://developer.apple.com/download/ + +## Building on OpenBSD + +You will need git, cmake, libexecinfo, libpng, gtk3mm and pangomm. +These can be installed from the ports tree: + +```sh +pkg_add -U git cmake libexecinfo png json-c gtk3mm pangomm +``` + +Before building, [check out the project and the necessary submodules](#via-source-code). + +After that, build SolveSpace as following: + +```sh +mkdir build +cd build +cmake .. -DCMAKE_BUILD_TYPE=Release +make +sudo make install +``` + +Unfortunately, on OpenBSD, the produced executables are not filesystem location independent +and must be installed before use. By default, the graphical interface is installed to +`/usr/local/bin/solvespace`, and the command-line interface is built as +`/usr/local/bin/solvespace-cli`. It is possible to build only the command-line interface +by passing the `-DENABLE_GUI=OFF` flag to the cmake invocation. + +## Building on Windows + +You will need [git][gitwin], [cmake][cmakewin] and a C++ compiler +(either Visual C++ or MinGW). If using Visual C++, Visual Studio 2015 +or later is required. +If gawk is in your path be sure it is a proper Windows port that can handle CL LF line endings. +If not CMake may fail in libpng due to some awk scripts - issue #1228. + +Before building, [check out the project and the necessary submodules](#via-source-code). + +### Building with Visual Studio IDE + +Create a directory `build` in +the source tree and point cmake-gui to the source tree and that directory. +Press "Configure" and "Generate", then open `build\solvespace.sln` with +Visual C++ and build it. + +### Building with Visual Studio in a command prompt + +First, ensure that `git` and `cl` (the Visual C++ compiler driver) are in your +`%PATH%`; the latter is usually done by invoking `vcvarsall.bat` from your +Visual Studio install. Then, run the following in cmd or PowerShell: + +```bat +mkdir build +cd build +cmake .. -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release +nmake +``` + +### Building with MinGW + +It is also possible to build SolveSpace using [MinGW][mingw], though +Space Navigator support will be disabled. + +The easiest way to build using MinGW is with [MSYS2][msys2]. If you're not using MSYS2, skip +the installation instructions and ensure that git, cmake, ninja, and gcc are in your `$PATH`. + +With MSYS2, you can build either a 32-bit binary or a 64-bit one, depending on the compiler +used. The following instructions assume you're running the commands inside an `MSYS2 MINGW64` +terminal window and building a 64-bit version. If you want to build a 32-bit version, you'll +need to run the commands in an `MSYS2 MINGW32` terminal window and replace `x86_64` +with `i686` in the installation commands. + +First, install Git, GCC, CMake, and Ninja: + +```sh +pacman -Sy mingw-w64-x86_64-git mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja +``` + +Then, run the following in bash: + +```sh +mkdir build +cd build +cmake .. -DCMAKE_BUILD_TYPE=Release -GNinja +ninja +``` + +[gitwin]: https://git-scm.com/download/win +[cmakewin]: https://www.cmake.org/download/#latest +[mingw]: http://www.mingw.org/ +[msys2]: https://www.msys2.org/ + +## Contributing + +See the [guide for contributors](CONTRIBUTING.md) for the best way to file issues, contribute code, +and debug SolveSpace. + +## License + +SolveSpace is distributed under the terms of the [GPL v3](COPYING.txt) or later. diff --git a/THIRD_PARTIES.txt b/THIRD_PARTIES.txt new file mode 100644 index 0000000..ad04bf6 --- /dev/null +++ b/THIRD_PARTIES.txt @@ -0,0 +1,50 @@ +This file outlines third party licenses that apply to the files and resources +listed with them. All file paths are relative to the SolveSpace project root. + +When redistributing SolveSpace, make sure to also reproduce this file such that +the paths indicating the scope of each license remain intact. +================================================================================ + + Bitstream Vera, ./res/fonts/BitstreamVeraSans-Roman-builtin.ttf + ~ +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a +trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +the fonts accompanying this license (“Fonts”) and associated documentation files +(the “Font Software”), to reproduce and distribute the Font Software, including +without limitation the rights to use, copy, merge, publish, distribute, and/or +sell copies of the Font Software, and to permit persons to whom the Font +Software is furnished to do so, subject to the following conditions: + +The above copyright and trademark notices and this permission notice shall be +included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular the +designs of glyphs or characters in the Fonts may be modified and additional +glyphs or characters may be added to the Fonts, only if the fonts are renamed to +names not containing either the words “Bitstream” or the word “Vera”. + +This License becomes null and void to the extent applicable to Fonts or Font +Software that has been modified and is distributed under the “Bitstream Vera” +names. + +The Font Software may be sold as part of a larger software package but no copy +of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR +OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, +INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR +FROM OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the names of GNOME, the GNOME Foundation, +and Bitstream Inc., shall not be used in advertising or otherwise to promote the +sale, use or other dealings in this Font Software without prior written +authorization from the GNOME Foundation or Bitstream Inc., respectively. For +further information, contact: fonts at gnome dot org. + + ----------- diff --git a/bench/CMakeLists.txt b/bench/CMakeLists.txt new file mode 100644 index 0000000..26172f1 --- /dev/null +++ b/bench/CMakeLists.txt @@ -0,0 +1,17 @@ +# benchmark runner + +foreach(pkg_config_lib CAIRO) + include_directories(${${pkg_config_lib}_INCLUDE_DIRS}) + link_directories(${${pkg_config_lib}_LIBRARY_DIRS}) +endforeach() + +add_executable(solvespace-benchmark + harness.cpp + $) + +target_link_libraries(solvespace-benchmark + solvespace-core + solvespace-headless) + +add_dependencies(solvespace-benchmark + resources) diff --git a/bench/harness.cpp b/bench/harness.cpp new file mode 100644 index 0000000..66d5308 --- /dev/null +++ b/bench/harness.cpp @@ -0,0 +1,85 @@ +//----------------------------------------------------------------------------- +// Our harness for running benchmarks. +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- +#include +#include +#include +#include + +#include "solvespace.h" + +using namespace SolveSpace; + +static bool RunBenchmark(std::function setupFn, + std::function benchFn, + std::function teardownFn, + size_t minIter = 5, double minTime = 5.0) { + // Warmup + setupFn(); + if(!benchFn()) { + fprintf(stderr, "Benchmark failed\n"); + return false; + } + teardownFn(); + + // Benchmark + size_t iter = 0; + double time = 0.0; + while(iter < minIter || time < minTime) { + setupFn(); + auto testStartTime = std::chrono::steady_clock::now(); + benchFn(); + auto testEndTime = std::chrono::steady_clock::now(); + teardownFn(); + + std::chrono::duration testTime = testEndTime - testStartTime; + time += testTime.count(); + iter += 1; + } + + // Report + fprintf(stdout, "Iterations: %zd\n", iter); + fprintf(stdout, "Time: %.3f s\n", time); + fprintf(stdout, "Per iter.: %.3f s\n", time / (double)iter); + + return true; +} + +int main(int argc, char **argv) { + std::vector args = Platform::InitCli(argc, argv); + + std::string mode; + Platform::Path filename; + if(args.size() == 3) { + mode = args[1]; + filename = Platform::Path::From(args[2]); + } else { + fprintf(stderr, "Usage: %s [mode] [filename]\n", args[0].c_str()); + fprintf(stderr, "Mode can be one of: load.\n"); + return 1; + } + + bool result = false; + if(mode == "load") { + result = RunBenchmark( + [] { + SS.Init(); + }, + [&] { + if(!SS.LoadFromFile(filename)) + return false; + SS.AfterNewFile(); + return true; + }, + [] { + SK.Clear(); + SS.Clear(); + }); + } else { + fprintf(stderr, "Unknown mode \"%s\"\n", mode.c_str()); + } + + return (result == true ? 0 : 1); +} diff --git a/cmake/AddVendoredSubdirectory.cmake b/cmake/AddVendoredSubdirectory.cmake new file mode 100644 index 0000000..9d2edf6 --- /dev/null +++ b/cmake/AddVendoredSubdirectory.cmake @@ -0,0 +1,10 @@ +# Equivalent to add_subdirectory(... EXCLUDE_FROM_ALL), but also disables +# all warnings. + +include(DisableWarnings) + +function(add_vendored_subdirectory PATH) + disable_warnings() + + add_subdirectory(${PATH} EXCLUDE_FROM_ALL) +endfunction() diff --git a/cmake/DisableWarnings.cmake b/cmake/DisableWarnings.cmake new file mode 100644 index 0000000..34925c8 --- /dev/null +++ b/cmake/DisableWarnings.cmake @@ -0,0 +1,15 @@ +# Disables all warnings on MSVC and GNU-compatible compilers. + +function(disable_warnings) + if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "Clang") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -w" PARENT_SCOPE) + elseif(CMAKE_C_COMPILER_ID STREQUAL "MSVC") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W0 /MP" PARENT_SCOPE) + endif() + + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -w" PARENT_SCOPE) + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W0 /MP" PARENT_SCOPE) + endif() +endfunction() diff --git a/cmake/FindCairo.cmake b/cmake/FindCairo.cmake new file mode 100644 index 0000000..d0130ad --- /dev/null +++ b/cmake/FindCairo.cmake @@ -0,0 +1,75 @@ +# - Try to find Cairo +# Once done, this will define +# +# CAIRO_FOUND - system has Cairo +# CAIRO_INCLUDE_DIRS - the Cairo include directories +# CAIRO_LIBRARIES - link these to use Cairo +# +# Copyright (C) 2012 Raphael Kubo da Costa +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND ITS CONTRIBUTORS ``AS +# IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR ITS +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +find_package(PkgConfig) +pkg_check_modules(PC_CAIRO QUIET cairo) + +find_path(CAIRO_INCLUDE_DIRS + NAMES cairo.h + HINTS ${PC_CAIRO_INCLUDEDIR} + ${PC_CAIRO_INCLUDE_DIRS} + PATH_SUFFIXES cairo +) + +find_library(CAIRO_LIBRARIES + NAMES cairo + HINTS ${PC_CAIRO_LIBDIR} + ${PC_CAIRO_LIBRARY_DIRS} +) + +if (CAIRO_INCLUDE_DIRS) + if (EXISTS "${CAIRO_INCLUDE_DIRS}/cairo-version.h") + file(READ "${CAIRO_INCLUDE_DIRS}/cairo-version.h" CAIRO_VERSION_CONTENT) + + string(REGEX MATCH "#define +CAIRO_VERSION_MAJOR +([0-9]+)" _dummy "${CAIRO_VERSION_CONTENT}") + set(CAIRO_VERSION_MAJOR "${CMAKE_MATCH_1}") + + string(REGEX MATCH "#define +CAIRO_VERSION_MINOR +([0-9]+)" _dummy "${CAIRO_VERSION_CONTENT}") + set(CAIRO_VERSION_MINOR "${CMAKE_MATCH_1}") + + string(REGEX MATCH "#define +CAIRO_VERSION_MICRO +([0-9]+)" _dummy "${CAIRO_VERSION_CONTENT}") + set(CAIRO_VERSION_MICRO "${CMAKE_MATCH_1}") + + set(CAIRO_VERSION "${CAIRO_VERSION_MAJOR}.${CAIRO_VERSION_MINOR}.${CAIRO_VERSION_MICRO}") + endif () +endif () + +if ("${Cairo_FIND_VERSION}" VERSION_GREATER "${CAIRO_VERSION}") + message(FATAL_ERROR "Required version (" ${Cairo_FIND_VERSION} ") is higher than found version (" ${CAIRO_VERSION} ")") +endif () + +include(FindPackageHandleStandardArgs) +FIND_PACKAGE_HANDLE_STANDARD_ARGS(Cairo REQUIRED_VARS CAIRO_INCLUDE_DIRS CAIRO_LIBRARIES + VERSION_VAR CAIRO_VERSION) + +mark_as_advanced( + CAIRO_INCLUDE_DIRS + CAIRO_LIBRARIES +) diff --git a/cmake/FindSpaceWare.cmake b/cmake/FindSpaceWare.cmake new file mode 100644 index 0000000..451063c --- /dev/null +++ b/cmake/FindSpaceWare.cmake @@ -0,0 +1,28 @@ +# Find the libspnav library and header. +# +# Sets the usual variables expected for find_package scripts: +# +# SPACEWARE_INCLUDE_DIR - header location +# SPACEWARE_LIBRARIES - library to link against +# SPACEWARE_FOUND - true if libspnav was found. + +if(UNIX) + + find_path(SPACEWARE_INCLUDE_DIR + spnav.h) + + find_library(SPACEWARE_LIBRARY + NAMES spnav libspnav) + + # Support the REQUIRED and QUIET arguments, and set SPACEWARE_FOUND if found. + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(SpaceWare DEFAULT_MSG + SPACEWARE_LIBRARY SPACEWARE_INCLUDE_DIR) + + if(SPACEWARE_FOUND) + set(SPACEWARE_LIBRARIES ${SPACEWARE_LIBRARY}) + endif() + + mark_as_advanced(SPACEWARE_LIBRARY SPACEWARE_INCLUDE_DIR) + +endif() diff --git a/cmake/FindVendoredPackage.cmake b/cmake/FindVendoredPackage.cmake new file mode 100644 index 0000000..6227f6c --- /dev/null +++ b/cmake/FindVendoredPackage.cmake @@ -0,0 +1,56 @@ +# Find the given library in the system locations, or build in-tree if not found. +# +# Arguments: +# PKG_NAME - name of the package as passed to find_package +# PKG_PATH - name of the source tree relative to extlib/ +# +# The rest of the arguments are VARIABLE VALUE pairs. If the library is not found, +# every VARIABLE will be set to VALUE and find_package will be rerun with the REQUIRED flag. +# Regardless of where the library was found, only the specified VARIABLEs that start with +# ${PKG_NAME} will be set in the parent scope. +# +# All warnings in the in-tree package are disabled. + +include(DisableWarnings) + +function(find_vendored_package PKG_NAME PKG_PATH) + if(NOT FORCE_VENDORED_${PKG_NAME}) + find_package(${PKG_NAME}) + endif() + + set(cfg_name) + foreach(item ${ARGN}) + if(NOT cfg_name) + set(cfg_name ${item}) + else() + set(${cfg_name} ${item} CACHE INTERNAL "") + set(cfg_name) + endif() + endforeach() + + disable_warnings() + + string(TOUPPER ${PKG_NAME} VAR_NAME) + if(NOT ${VAR_NAME}_FOUND) + message(STATUS "Using in-tree ${PKG_PATH}") + set(${VAR_NAME}_IN_TREE YES CACHE INTERNAL "") + + add_subdirectory(extlib/${PKG_PATH} EXCLUDE_FROM_ALL) + find_package(${PKG_NAME} REQUIRED) + elseif(${VAR_NAME}_IN_TREE) + add_subdirectory(extlib/${PKG_PATH} EXCLUDE_FROM_ALL) + endif() + + # Now put everything we just discovered into the cache. + set(cfg_name) + foreach(item ${ARGN} ${VAR_NAME}_FOUND) + if(NOT cfg_name) + set(cfg_name ${item}) + else() + if(cfg_name MATCHES "^${VAR_NAME}") + set(${cfg_name} "${${cfg_name}}" CACHE INTERNAL "") + endif() + set(cfg_name) + endif() + endforeach() +endfunction() diff --git a/cmake/GetGitCommitHash.cmake b/cmake/GetGitCommitHash.cmake new file mode 100644 index 0000000..c3d2267 --- /dev/null +++ b/cmake/GetGitCommitHash.cmake @@ -0,0 +1,43 @@ +function(get_git_commit_hash) + get_filename_component(GIT_DESCRIBE_CMAKE_DIR ${CMAKE_CURRENT_LIST_FILE} PATH) + get_filename_component(GIT_ROOT ${GIT_DESCRIBE_CMAKE_DIR} PATH) + set(GIT_DIR "${GIT_ROOT}/.git") + + if(EXISTS ${GIT_DIR} AND NOT IS_DIRECTORY ${GIT_DIR}) + # In case we are included as a git submodule. + file(READ ${GIT_DIR} GIT_SUB_DIR) + string(REPLACE "gitdir: " "" GIT_SUB_DIR ${GIT_SUB_DIR}) + string(STRIP ${GIT_SUB_DIR} GIT_SUB_DIR) + set(GIT_DIR "${GIT_ROOT}/${GIT_SUB_DIR}") + endif() + + # Add a CMake configure dependency to the currently checked out revision. + set(GIT_DEPENDS ${GIT_DIR}/HEAD) + file(READ ${GIT_DIR}/HEAD HEAD_REF) + if(HEAD_REF MATCHES "ref: (.+)\n") + set(HEAD_REF ${CMAKE_MATCH_1}) + if(EXISTS "${GIT_DIR}/${HEAD_REF}") + list(APPEND GIT_DEPENDS ${GIT_DIR}/${HEAD_REF}) + file(READ ${GIT_DIR}/${HEAD_REF} HEAD_REF) + elseif(EXISTS "${GIT_DIR}/packed-refs") + list(APPEND GIT_DEPENDS ${GIT_DIR}/packed-refs) + file(READ "${GIT_DIR}/packed-refs" PACKED_REFS) + if(${PACKED_REFS} MATCHES "([0-9a-z]*) ${HEAD_REF}") + set(HEAD_REF ${CMAKE_MATCH_1}) + else() + set(HEAD_REF "") + endif() + else() + set(HEAD_REF "") + endif() + endif() + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${GIT_DEPENDS}) + + string(STRIP ${HEAD_REF} HEAD_REF) + if(HEAD_REF STREQUAL "") + message(WARNING "Cannot determine git HEAD") + else() + set(GIT_COMMIT_HASH ${HEAD_REF} PARENT_SCOPE) + endif() +endfunction() +get_git_commit_hash() diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in new file mode 100644 index 0000000..dddb46f --- /dev/null +++ b/cmake/MacOSXBundleInfo.plist.in @@ -0,0 +1,49 @@ + + + + + CFBundleIdentifier + com.solvespace + CFBundleDevelopmentRegion + English + CFBundleExecutable + SolveSpace + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + SolveSpace + CFBundlePackageType + APPL + CFBundleVersion + ${PROJECT_VERSION}~${solvespace_GIT_HASH} + CFBundleShortVersionString + ${PROJECT_VERSION} + NSHumanReadableCopyright + © 2008-2026 Jonathan Westhues and other authors + NSPrincipalClass + NSApplication + NSMainNibFile + MainMenu + CFBundleIconFile + AppIcon + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + slvs + + CFBundleTypeIconFile + AppIcon.icns + CFBundleTypeName + SolveSpace sketch + CFBundleTypeOSTypes + + slvs + + CFBundleTypeRole + Editor + + + + diff --git a/cmake/Platform/Emscripten.cmake b/cmake/Platform/Emscripten.cmake new file mode 100644 index 0000000..9ad0a77 --- /dev/null +++ b/cmake/Platform/Emscripten.cmake @@ -0,0 +1,364 @@ +# This file is a 'toolchain description file' for CMake. +# It teaches CMake about the Emscripten compiler, so that CMake can generate +# makefiles from CMakeLists.txt that invoke emcc. + +# The following variable describes the target OS we are building to. +set(CMAKE_SYSTEM_NAME Emscripten) +set(CMAKE_SYSTEM_VERSION 1) + +set(CMAKE_CROSSCOMPILING TRUE) +set_property(GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS FALSE) + +# Advertise Emscripten as a 32-bit platform (as opposed to +# CMAKE_SYSTEM_PROCESSOR=x86_64 for 64-bit platform), since some projects (e.g. +# OpenCV) use this to detect bitness. +# Allow users to ovewrite this on the command line with -DEMSCRIPTEN_SYSTEM_PROCESSOR=arm. +if (NOT DEFINED EMSCRIPTEN_SYSTEM_PROCESSOR) + set(EMSCRIPTEN_SYSTEM_PROCESSOR x86) +endif() +set(CMAKE_SYSTEM_PROCESSOR ${EMSCRIPTEN_SYSTEM_PROCESSOR}) + +# Tell CMake how it should instruct the compiler to generate multiple versions +# of an outputted .so library: e.g. "libfoo.so, libfoo.so.1, libfoo.so.1.4" etc. +# This feature is activated if a shared library project has the property +# SOVERSION defined. +set(CMAKE_SHARED_LIBRARY_SONAME_C_FLAG "-Wl,-soname,") + +# In CMake, CMAKE_HOST_WIN32 is set when we are cross-compiling from Win32 to +# Emscripten: +# http://www.cmake.org/cmake/help/v2.8.12/cmake.html#variable:CMAKE_HOST_WIN32 +# The variable WIN32 is set only when the target arch that will run the code +# will be WIN32, so unset WIN32 when cross-compiling. +set(WIN32) + +# The same logic as above applies for APPLE and CMAKE_HOST_APPLE, so unset +# APPLE. +set(APPLE) + +# And for UNIX and CMAKE_HOST_UNIX. However, Emscripten is often able to mimic +# being a Linux/Unix system, in which case a lot of existing CMakeLists.txt +# files can be configured for Emscripten while assuming UNIX build, so this is +# left enabled. +set(UNIX 1) + +# Do a no-op access on the CMAKE_TOOLCHAIN_FILE variable so that CMake will not +# issue a warning on it being unused. +if (CMAKE_TOOLCHAIN_FILE) +endif() + +if (CMAKE_HOST_WIN32) + set(EMCC_SUFFIX ".bat") +else() + set(EMCC_SUFFIX "") +endif() + +# Locate where the Emscripten compiler resides in +if (NOT DEFINED EMSCRIPTEN_ROOT_PATH) + # Check relative to this toolchain file + get_filename_component(GUESS_EMSCRIPTEN_ROOT_PATH "${CMAKE_CURRENT_LIST_DIR}/../../../" ABSOLUTE) + # If not found by above search, locate using the EMSCRIPTEN environment variable + find_program(EMSCRIPTEN_EMRANLIB_PATH emranlib${EMCC_SUFFIX} HINTS "${GUESS_EMSCRIPTEN_ROOT_PATH}" ENV "EMSCRIPTEN") + if(NOT EMSCRIPTEN_EMRANLIB_PATH) + # Abort if not found + message(FATAL_ERROR "Could not locate the Emscripten compiler toolchain directory! Either set the EMSCRIPTEN environment variable, or pass -DEMSCRIPTEN_ROOT_PATH=xxx to CMake to explicitly specify the location of the compiler!") + endif() + get_filename_component(EMSCRIPTEN_ROOT_PATH "${EMSCRIPTEN_EMRANLIB_PATH}" DIRECTORY) +endif() + +# Normalize, convert Windows backslashes to forward slashes or CMake will crash. +get_filename_component(EMSCRIPTEN_ROOT_PATH "${EMSCRIPTEN_ROOT_PATH}" ABSOLUTE) + +list(APPEND CMAKE_MODULE_PATH "${EMSCRIPTEN_ROOT_PATH}/cmake/Modules") + +# Specify the compilers to use for C and C++ +set(CMAKE_C_COMPILER "${EMSCRIPTEN_ROOT_PATH}/emcc${EMCC_SUFFIX}") +set(CMAKE_CXX_COMPILER "${EMSCRIPTEN_ROOT_PATH}/em++${EMCC_SUFFIX}") +set(CMAKE_NM "${EMSCRIPTEN_ROOT_PATH}/emnm${EMCC_SUFFIX}") +set(CMAKE_AR "${EMSCRIPTEN_ROOT_PATH}/emar${EMCC_SUFFIX}") +set(CMAKE_RANLIB "${EMSCRIPTEN_ROOT_PATH}/emranlib${EMCC_SUFFIX}") +set(CMAKE_C_COMPILER_AR "${CMAKE_AR}") +set(CMAKE_CXX_COMPILER_AR "${CMAKE_AR}") +set(CMAKE_C_COMPILER_RANLIB "${CMAKE_RANLIB}") +set(CMAKE_CXX_COMPILER_RANLIB "${CMAKE_RANLIB}") +set(CMAKE_CXX_COMPILER_CLANG_SCAN_DEPS "${EMSCRIPTEN_ROOT_PATH}/emscan-deps") + +# Capture the Emscripten version to EMSCRIPTEN_VERSION variable. +if (NOT EMSCRIPTEN_VERSION) + execute_process(COMMAND "${CMAKE_C_COMPILER}" "-v" RESULT_VARIABLE _cmake_compiler_result ERROR_VARIABLE _cmake_compiler_output OUTPUT_QUIET) + if (NOT _cmake_compiler_result EQUAL 0) + message(FATAL_ERROR "Failed to fetch Emscripten version information with command \"'${CMAKE_C_COMPILER}' -v\"!\n" + "Process returned with error code ${_cmake_compiler_result}.\n" + "Output:\n${_cmake_compiler_output}") + endif() + string(REGEX MATCH "emcc \\(.*\\) ([0-9\\.]+)" _dummy_unused "${_cmake_compiler_output}") + if (NOT CMAKE_MATCH_1) + message(FATAL_ERROR "Failed to regex parse Emscripten compiler version from version string: ${_cmake_compiler_output}") + endif() + + set(EMSCRIPTEN_VERSION "${CMAKE_MATCH_1}") +endif() + +execute_process(COMMAND "${EMSCRIPTEN_ROOT_PATH}/em-config${EMCC_SUFFIX}" "CACHE" + RESULT_VARIABLE _emcache_result + OUTPUT_VARIABLE _emcache_output + OUTPUT_STRIP_TRAILING_WHITESPACE) +if (NOT _emcache_result EQUAL 0) + message(FATAL_ERROR "Failed to find emscripten cache directory with command \"'${EMSCRIPTEN_ROOT_PATH}/em-config${EMCC_SUFFIX}' CACHE\"! Process returned with error code ${_emcache_result}.") +endif() +file(TO_CMAKE_PATH "${_emcache_output}" _emcache_output) +set(EMSCRIPTEN_SYSROOT "${_emcache_output}/sysroot") + +# Allow skipping of CMake compiler autodetection, since this is quite slow with +# Emscripten. Pass -DEMSCRIPTEN_FORCE_COMPILERS=ON to enable +option(EMSCRIPTEN_FORCE_COMPILERS "Force C/C++ compiler" OFF) +if (EMSCRIPTEN_FORCE_COMPILERS) + # Detect version of the 'emcc' executable. Note that for CMake, we tell it the + # version of the Clang compiler and not the version of Emscripten, because + # CMake understands Clang better. + # Toolchain script is interpreted multiple times, so don't rerun the check if + # already done before. + if (NOT CMAKE_C_COMPILER_VERSION) + execute_process(COMMAND "${CMAKE_C_COMPILER}" "-v" RESULT_VARIABLE _cmake_compiler_result ERROR_VARIABLE _cmake_compiler_output OUTPUT_QUIET) + if (NOT _cmake_compiler_result EQUAL 0) + message(FATAL_ERROR "Failed to fetch compiler version information with command \"'${CMAKE_C_COMPILER}' -v\"! Process returned with error code ${_cmake_compiler_result}.") + endif() + if (NOT "${_cmake_compiler_output}" MATCHES "[Ee]mscripten") + message(FATAL_ERROR "System LLVM compiler cannot be used to build with Emscripten! Check Emscripten's LLVM toolchain location in .emscripten configuration file, and make sure to point CMAKE_C_COMPILER to where emcc is located. (was pointing to \"${CMAKE_C_COMPILER}\")") + endif() + string(REGEX MATCH "clang version ([0-9\\.]+)" _dummy_unused "${_cmake_compiler_output}") + if (NOT CMAKE_MATCH_1) + message(FATAL_ERROR "Failed to regex parse Clang compiler version from version string: ${_cmake_compiler_output}") + endif() + + set(CMAKE_C_COMPILER_VERSION "${CMAKE_MATCH_1}") + set(CMAKE_CXX_COMPILER_VERSION "${CMAKE_MATCH_1}") + if (${CMAKE_C_COMPILER_VERSION} VERSION_LESS 3.9.0) + message(WARNING "CMAKE_C_COMPILER version looks too old. Was ${CMAKE_C_COMPILER_VERSION}, should be at least 3.9.0.") + endif() + endif() + + set(CMAKE_C_COMPILER_ID_RUN TRUE) + set(CMAKE_C_COMPILER_FORCED TRUE) + set(CMAKE_C_COMPILER_WORKS TRUE) + set(CMAKE_C_COMPILER_ID Clang) + set(CMAKE_C_COMPILER_FRONTEND_VARIANT GNU) + set(CMAKE_C_STANDARD_COMPUTED_DEFAULT 11) + + set(CMAKE_CXX_COMPILER_ID_RUN TRUE) + set(CMAKE_CXX_COMPILER_FORCED TRUE) + set(CMAKE_CXX_COMPILER_WORKS TRUE) + set(CMAKE_CXX_COMPILER_ID Clang) + set(CMAKE_CXX_COMPILER_FRONTEND_VARIANT GNU) + set(CMAKE_CXX_STANDARD_COMPUTED_DEFAULT 98) + + set(CMAKE_C_PLATFORM_ID "emscripten") + set(CMAKE_CXX_PLATFORM_ID "emscripten") + + if (NOT DEFINED CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES) + set(CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES "${EMSCRIPTEN_SYSROOT}/include") + endif() + if (NOT DEFINED CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES) + set(CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES "${EMSCRIPTEN_SYSROOT}/include;${EMSCRIPTEN_SYSROOT}/include/c++/v1") + endif() + + if ("${CMAKE_VERSION}" VERSION_LESS "3.8") + set(CMAKE_C_COMPILE_FEATURES "c_function_prototypes;c_restrict;c_variadic_macros;c_static_assert") + set(CMAKE_C90_COMPILE_FEATURES "c_function_prototypes") + set(CMAKE_C99_COMPILE_FEATURES "c_restrict;c_variadic_macros") + set(CMAKE_C11_COMPILE_FEATURES "c_static_assert") + + set(CMAKE_CXX_COMPILE_FEATURES "cxx_template_template_parameters;cxx_alias_templates;cxx_alignas;cxx_alignof;cxx_attributes;cxx_auto_type;cxx_constexpr;cxx_decltype;cxx_decltype_incomplete_return_types;cxx_default_function_template_args;cxx_defaulted_functions;cxx_defaulted_move_initializers;cxx_delegating_constructors;cxx_deleted_functions;cxx_enum_forward_declarations;cxx_explicit_conversions;cxx_extended_friend_declarations;cxx_extern_templates;cxx_final;cxx_func_identifier;cxx_generalized_initializers;cxx_inheriting_constructors;cxx_inline_namespaces;cxx_lambdas;cxx_local_type_template_args;cxx_long_long_type;cxx_noexcept;cxx_nonstatic_member_init;cxx_nullptr;cxx_override;cxx_range_for;cxx_raw_string_literals;cxx_reference_qualified_functions;cxx_right_angle_brackets;cxx_rvalue_references;cxx_sizeof_member;cxx_static_assert;cxx_strong_enums;cxx_thread_local;cxx_trailing_return_types;cxx_unicode_literals;cxx_uniform_initialization;cxx_unrestricted_unions;cxx_user_literals;cxx_variadic_macros;cxx_variadic_templates;cxx_aggregate_default_initializers;cxx_attribute_deprecated;cxx_binary_literals;cxx_contextual_conversions;cxx_decltype_auto;cxx_digit_separators;cxx_generic_lambdas;cxx_lambda_init_captures;cxx_relaxed_constexpr;cxx_return_type_deduction;cxx_variable_templates") + set(CMAKE_CXX98_COMPILE_FEATURES "cxx_template_template_parameters") + set(CMAKE_CXX11_COMPILE_FEATURES "cxx_alias_templates;cxx_alignas;cxx_alignof;cxx_attributes;cxx_auto_type;cxx_constexpr;cxx_decltype;cxx_decltype_incomplete_return_types;cxx_default_function_template_args;cxx_defaulted_functions;cxx_defaulted_move_initializers;cxx_delegating_constructors;cxx_deleted_functions;cxx_enum_forward_declarations;cxx_explicit_conversions;cxx_extended_friend_declarations;cxx_extern_templates;cxx_final;cxx_func_identifier;cxx_generalized_initializers;cxx_inheriting_constructors;cxx_inline_namespaces;cxx_lambdas;cxx_local_type_template_args;cxx_long_long_type;cxx_noexcept;cxx_nonstatic_member_init;cxx_nullptr;cxx_override;cxx_range_for;cxx_raw_string_literals;cxx_reference_qualified_functions;cxx_right_angle_brackets;cxx_rvalue_references;cxx_sizeof_member;cxx_static_assert;cxx_strong_enums;cxx_thread_local;cxx_trailing_return_types;cxx_unicode_literals;cxx_uniform_initialization;cxx_unrestricted_unions;cxx_user_literals;cxx_variadic_macros;cxx_variadic_templates") + set(CMAKE_CXX14_COMPILE_FEATURES "cxx_aggregate_default_initializers;cxx_attribute_deprecated;cxx_binary_literals;cxx_contextual_conversions;cxx_decltype_auto;cxx_digit_separators;cxx_generic_lambdas;cxx_lambda_init_captures;cxx_relaxed_constexpr;cxx_return_type_deduction;cxx_variable_templates") + else() # 3.8+ + set(CMAKE_C90_COMPILE_FEATURES "c_std_90;c_function_prototypes") + set(CMAKE_C99_COMPILE_FEATURES "c_std_99;c_restrict;c_variadic_macros") + set(CMAKE_C11_COMPILE_FEATURES "c_std_11;c_static_assert") + + set(CMAKE_CXX98_COMPILE_FEATURES "cxx_std_98;cxx_template_template_parameters") + set(CMAKE_CXX11_COMPILE_FEATURES "cxx_std_11;cxx_alias_templates;cxx_alignas;cxx_alignof;cxx_attributes;cxx_auto_type;cxx_constexpr;cxx_decltype;cxx_decltype_incomplete_return_types;cxx_default_function_template_args;cxx_defaulted_functions;cxx_defaulted_move_initializers;cxx_delegating_constructors;cxx_deleted_functions;cxx_enum_forward_declarations;cxx_explicit_conversions;cxx_extended_friend_declarations;cxx_extern_templates;cxx_final;cxx_func_identifier;cxx_generalized_initializers;cxx_inheriting_constructors;cxx_inline_namespaces;cxx_lambdas;cxx_local_type_template_args;cxx_long_long_type;cxx_noexcept;cxx_nonstatic_member_init;cxx_nullptr;cxx_override;cxx_range_for;cxx_raw_string_literals;cxx_reference_qualified_functions;cxx_right_angle_brackets;cxx_rvalue_references;cxx_sizeof_member;cxx_static_assert;cxx_strong_enums;cxx_thread_local;cxx_trailing_return_types;cxx_unicode_literals;cxx_uniform_initialization;cxx_unrestricted_unions;cxx_user_literals;cxx_variadic_macros;cxx_variadic_templates") + set(CMAKE_CXX14_COMPILE_FEATURES "cxx_std_14;cxx_aggregate_default_initializers;cxx_attribute_deprecated;cxx_binary_literals;cxx_contextual_conversions;cxx_decltype_auto;cxx_digit_separators;cxx_generic_lambdas;cxx_lambda_init_captures;cxx_relaxed_constexpr;cxx_return_type_deduction;cxx_variable_templates") + set(CMAKE_CXX17_COMPILE_FEATURES "cxx_std_17") + if ("${CMAKE_VERSION}" VERSION_LESS "3.12") # [3.8, 3.12) + set(CMAKE_C_COMPILE_FEATURES "c_std_90;c_function_prototypes;c_std_99;c_restrict;c_variadic_macros;c_std_11;c_static_assert") + set(CMAKE_CXX_COMPILE_FEATURES "cxx_std_98;cxx_template_template_parameters;cxx_std_11;cxx_alias_templates;cxx_alignas;cxx_alignof;cxx_attributes;cxx_auto_type;cxx_constexpr;cxx_decltype;cxx_decltype_incomplete_return_types;cxx_default_function_template_args;cxx_defaulted_functions;cxx_defaulted_move_initializers;cxx_delegating_constructors;cxx_deleted_functions;cxx_enum_forward_declarations;cxx_explicit_conversions;cxx_extended_friend_declarations;cxx_extern_templates;cxx_final;cxx_func_identifier;cxx_generalized_initializers;cxx_inheriting_constructors;cxx_inline_namespaces;cxx_lambdas;cxx_local_type_template_args;cxx_long_long_type;cxx_noexcept;cxx_nonstatic_member_init;cxx_nullptr;cxx_override;cxx_range_for;cxx_raw_string_literals;cxx_reference_qualified_functions;cxx_right_angle_brackets;cxx_rvalue_references;cxx_sizeof_member;cxx_static_assert;cxx_strong_enums;cxx_thread_local;cxx_trailing_return_types;cxx_unicode_literals;cxx_uniform_initialization;cxx_unrestricted_unions;cxx_user_literals;cxx_variadic_macros;cxx_variadic_templates;cxx_std_14;cxx_aggregate_default_initializers;cxx_attribute_deprecated;cxx_binary_literals;cxx_contextual_conversions;cxx_decltype_auto;cxx_digit_separators;cxx_generic_lambdas;cxx_lambda_init_captures;cxx_relaxed_constexpr;cxx_return_type_deduction;cxx_variable_templates;cxx_std_17") + else() # 3.12+ + set(CMAKE_CXX20_COMPILE_FEATURES "cxx_std_20") + if ("${CMAKE_VERSION}" VERSION_LESS "3.20") # [3.12, 3.20) + set(CMAKE_C_COMPILE_FEATURES "c_std_90;c_function_prototypes;c_std_99;c_restrict;c_variadic_macros;c_std_11;c_static_assert") + set(CMAKE_CXX_COMPILE_FEATURES "cxx_std_98;cxx_template_template_parameters;cxx_std_11;cxx_alias_templates;cxx_alignas;cxx_alignof;cxx_attributes;cxx_auto_type;cxx_constexpr;cxx_decltype;cxx_decltype_incomplete_return_types;cxx_default_function_template_args;cxx_defaulted_functions;cxx_defaulted_move_initializers;cxx_delegating_constructors;cxx_deleted_functions;cxx_enum_forward_declarations;cxx_explicit_conversions;cxx_extended_friend_declarations;cxx_extern_templates;cxx_final;cxx_func_identifier;cxx_generalized_initializers;cxx_inheriting_constructors;cxx_inline_namespaces;cxx_lambdas;cxx_local_type_template_args;cxx_long_long_type;cxx_noexcept;cxx_nonstatic_member_init;cxx_nullptr;cxx_override;cxx_range_for;cxx_raw_string_literals;cxx_reference_qualified_functions;cxx_right_angle_brackets;cxx_rvalue_references;cxx_sizeof_member;cxx_static_assert;cxx_strong_enums;cxx_thread_local;cxx_trailing_return_types;cxx_unicode_literals;cxx_uniform_initialization;cxx_unrestricted_unions;cxx_user_literals;cxx_variadic_macros;cxx_variadic_templates;cxx_std_14;cxx_aggregate_default_initializers;cxx_attribute_deprecated;cxx_binary_literals;cxx_contextual_conversions;cxx_decltype_auto;cxx_digit_separators;cxx_generic_lambdas;cxx_lambda_init_captures;cxx_relaxed_constexpr;cxx_return_type_deduction;cxx_variable_templates;cxx_std_17;cxx_std_20") + else() # 3.20+ + set(CMAKE_CXX23_COMPILE_FEATURES "cxx_std_23") + if ("${CMAKE_VERSION}" VERSION_LESS "3.25") + set(CMAKE_CXX_COMPILE_FEATURES "cxx_std_98;cxx_template_template_parameters;cxx_std_11;cxx_alias_templates;cxx_alignas;cxx_alignof;cxx_attributes;cxx_auto_type;cxx_constexpr;cxx_decltype;cxx_decltype_incomplete_return_types;cxx_default_function_template_args;cxx_defaulted_functions;cxx_defaulted_move_initializers;cxx_delegating_constructors;cxx_deleted_functions;cxx_enum_forward_declarations;cxx_explicit_conversions;cxx_extended_friend_declarations;cxx_extern_templates;cxx_final;cxx_func_identifier;cxx_generalized_initializers;cxx_inheriting_constructors;cxx_inline_namespaces;cxx_lambdas;cxx_local_type_template_args;cxx_long_long_type;cxx_noexcept;cxx_nonstatic_member_init;cxx_nullptr;cxx_override;cxx_range_for;cxx_raw_string_literals;cxx_reference_qualified_functions;cxx_right_angle_brackets;cxx_rvalue_references;cxx_sizeof_member;cxx_static_assert;cxx_strong_enums;cxx_thread_local;cxx_trailing_return_types;cxx_unicode_literals;cxx_uniform_initialization;cxx_unrestricted_unions;cxx_user_literals;cxx_variadic_macros;cxx_variadic_templates;cxx_std_14;cxx_aggregate_default_initializers;cxx_attribute_deprecated;cxx_binary_literals;cxx_contextual_conversions;cxx_decltype_auto;cxx_digit_separators;cxx_generic_lambdas;cxx_lambda_init_captures;cxx_relaxed_constexpr;cxx_return_type_deduction;cxx_variable_templates;cxx_std_17;cxx_std_20;cxx_std_23") + else() # 3.25+ + set(CMAKE_CXX_COMPILE_FEATURES "cxx_std_98;cxx_template_template_parameters;cxx_std_11;cxx_alias_templates;cxx_alignas;cxx_alignof;cxx_attributes;cxx_auto_type;cxx_constexpr;cxx_decltype;cxx_decltype_incomplete_return_types;cxx_default_function_template_args;cxx_defaulted_functions;cxx_defaulted_move_initializers;cxx_delegating_constructors;cxx_deleted_functions;cxx_enum_forward_declarations;cxx_explicit_conversions;cxx_extended_friend_declarations;cxx_extern_templates;cxx_final;cxx_func_identifier;cxx_generalized_initializers;cxx_inheriting_constructors;cxx_inline_namespaces;cxx_lambdas;cxx_local_type_template_args;cxx_long_long_type;cxx_noexcept;cxx_nonstatic_member_init;cxx_nullptr;cxx_override;cxx_range_for;cxx_raw_string_literals;cxx_reference_qualified_functions;cxx_right_angle_brackets;cxx_rvalue_references;cxx_sizeof_member;cxx_static_assert;cxx_strong_enums;cxx_thread_local;cxx_trailing_return_types;cxx_unicode_literals;cxx_uniform_initialization;cxx_unrestricted_unions;cxx_user_literals;cxx_variadic_macros;cxx_variadic_templates;cxx_std_14;cxx_aggregate_default_initializers;cxx_attribute_deprecated;cxx_binary_literals;cxx_contextual_conversions;cxx_decltype_auto;cxx_digit_separators;cxx_generic_lambdas;cxx_lambda_init_captures;cxx_relaxed_constexpr;cxx_return_type_deduction;cxx_variable_templates;cxx_std_17;cxx_std_20;cxx_std_23;cxx_std_26") + endif() + if ("${CMAKE_VERSION}" VERSION_LESS "3.21") # 3.20 + set(CMAKE_C_COMPILE_FEATURES "c_std_90;c_function_prototypes;c_std_99;c_restrict;c_variadic_macros;c_std_11;c_static_assert") + else() # 3.21+ + set(CMAKE_C17_COMPILE_FEATURES "c_std_17") + set(CMAKE_C23_COMPILE_FEATURES "c_std_23") + set(CMAKE_C_COMPILE_FEATURES "c_std_90;c_function_prototypes;c_std_99;c_restrict;c_variadic_macros;c_std_11;c_static_assert;c_std_17;c_std_23") + endif() + endif() + endif() + endif() +endif() + +list(APPEND CMAKE_FIND_ROOT_PATH "${EMSCRIPTEN_SYSROOT}") +list(APPEND CMAKE_SYSTEM_PREFIX_PATH /) + +if (${CMAKE_C_FLAGS} MATCHES "MEMORY64") + set(CMAKE_LIBRARY_ARCHITECTURE "wasm64-emscripten") + set(CMAKE_SIZEOF_VOID_P 8) + set(CMAKE_C_SIZEOF_DATA_PTR 8) + set(CMAKE_CXX_SIZEOF_DATA_PTR 8) +else() + set(CMAKE_LIBRARY_ARCHITECTURE "wasm32-emscripten") + set(CMAKE_SIZEOF_VOID_P 4) + set(CMAKE_C_SIZEOF_DATA_PTR 4) + set(CMAKE_CXX_SIZEOF_DATA_PTR 4) +endif() + +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${EMSCRIPTEN_SYSROOT}" CACHE PATH + "Install path prefix, prepended onto install directories." FORCE) +endif() + +# To find programs to execute during CMake run time with find_program(), e.g. +# 'git' or so, we allow looking into system paths. +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + +# Since Emscripten is a cross-compiler, we should never look at the +# system-provided directories like /usr/include and so on. Therefore only +# CMAKE_FIND_ROOT_PATH should be used as a find directory. See +# http://www.cmake.org/cmake/help/v3.0/variable/CMAKE_FIND_ROOT_PATH_MODE_INCLUDE.html +if (NOT CMAKE_FIND_ROOT_PATH_MODE_LIBRARY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +endif() +if (NOT CMAKE_FIND_ROOT_PATH_MODE_INCLUDE) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() +if (NOT CMAKE_FIND_ROOT_PATH_MODE_PACKAGE) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) +endif() + +set(_em_pkgconfig_libdir "${EMSCRIPTEN_SYSROOT}/local/lib/pkgconfig" "${EMSCRIPTEN_SYSROOT}/lib/pkgconfig") +if("${CMAKE_VERSION}" VERSION_LESS "3.20") + file(TO_NATIVE_PATH "${_em_pkgconfig_libdir}" _em_pkgconfig_libdir) + if(CMAKE_HOST_UNIX) + string(REPLACE ";" ":" _em_pkgconfig_libdir "${_em_pkgconfig_libdir}") + string(REPLACE "\\ " " " _em_pkgconfig_libdir "${_em_pkgconfig_libdir}") + endif() +else() + cmake_path(CONVERT "${_em_pkgconfig_libdir}" TO_NATIVE_PATH_LIST _em_pkgconfig_libdir) +endif() +set(ENV{PKG_CONFIG_LIBDIR} "${_em_pkgconfig_libdir}") +unset(_em_pkgconfig_libdir) + +option(EMSCRIPTEN_GENERATE_BITCODE_STATIC_LIBRARIES "If set, static library targets generate LLVM bitcode files (.bc). If disabled (default), UNIX ar archives (.a) are generated." OFF) +if (EMSCRIPTEN_GENERATE_BITCODE_STATIC_LIBRARIES) + message(FATAL_ERROR "EMSCRIPTEN_GENERATE_BITCODE_STATIC_LIBRARIES is not compatible with the llvm backend") +endif() + +set(CMAKE_EXECUTABLE_SUFFIX ".js") + +set(CMAKE_C_USE_RESPONSE_FILE_FOR_LIBRARIES 1) +set(CMAKE_CXX_USE_RESPONSE_FILE_FOR_LIBRARIES 1) +set(CMAKE_C_USE_RESPONSE_FILE_FOR_OBJECTS 1) +set(CMAKE_CXX_USE_RESPONSE_FILE_FOR_OBJECTS 1) +set(CMAKE_C_USE_RESPONSE_FILE_FOR_INCLUDES 1) +set(CMAKE_CXX_USE_RESPONSE_FILE_FOR_INCLUDES 1) + +set(CMAKE_C_RESPONSE_FILE_LINK_FLAG "@") +set(CMAKE_CXX_RESPONSE_FILE_LINK_FLAG "@") + +# Enable $ for CMake 3.24+ +set(CMAKE_LINK_LIBRARY_USING_WHOLE_ARCHIVE "-Wl,--whole-archive" "" "-Wl,--no-whole-archive") +set(CMAKE_LINK_LIBRARY_USING_WHOLE_ARCHIVE_SUPPORTED True) + +# Set a global EMSCRIPTEN variable that can be used in client CMakeLists.txt to +# detect when building using Emscripten. +set(EMSCRIPTEN 1 CACHE INTERNAL "If true, we are targeting Emscripten output.") + +# Hardwire support for cmake-2.8/Modules/CMakeBackwardsCompatibilityC.cmake +# without having CMake to try complex things to autodetect these: +set(CMAKE_SKIP_COMPATIBILITY_TESTS 1) +set(CMAKE_SIZEOF_CHAR 1) +set(CMAKE_SIZEOF_UNSIGNED_SHORT 2) +set(CMAKE_SIZEOF_SHORT 2) +set(CMAKE_SIZEOF_INT 4) +set(CMAKE_SIZEOF_UNSIGNED_LONG 4) +set(CMAKE_SIZEOF_UNSIGNED_INT 4) +set(CMAKE_SIZEOF_LONG 4) +set(CMAKE_SIZEOF_FLOAT 4) +set(CMAKE_SIZEOF_DOUBLE 8) +set(CMAKE_HAVE_LIMITS_H 1) +set(CMAKE_HAVE_UNISTD_H 1) +set(CMAKE_HAVE_PTHREAD_H 1) +set(CMAKE_HAVE_SYS_PRCTL_H 1) +set(CMAKE_WORDS_BIGENDIAN 0) +set(CMAKE_C_BYTE_ORDER "LITTLE_ENDIAN") +set(CMAKE_CXX_BYTE_ORDER "LITTLE_ENDIAN") +set(CMAKE_DL_LIBS) + +function(em_validate_asmjs_after_build target) + message(WARNING "em_validate_asmjs_after_build no longer exists") +endfunction() + +# Internal function: Do not call from user CMakeLists.txt files. Use one of +# em_link_js_library()/em_link_pre_js()/em_link_post_js() instead. +function(em_add_link_deps target flagname) + # User can input list of JS files either as a single list, or as variable + # arguments to this function, so iterate over varargs, and treat each item in + # varargs as a list itself, to support both syntax forms. + foreach(jsFileList ${ARGN}) + foreach(jsfile ${jsFileList}) + get_target_property(linkdeps ${target} LINK_DEPENDS) + if(linkdeps STREQUAL "linkdeps-NOTFOUND") + set(linkdeps "") + endif() + get_filename_component(jsfile_abs "${jsfile}" ABSOLUTE ) + set_target_properties(${target} PROPERTIES LINK_DEPENDS "${linkdeps};${jsfile_abs}") + target_link_libraries(${target} "${flagname} \"${jsfile_abs}\"") + endforeach() + endforeach() +endfunction() + +# This function links a (list of ) .js library file(s) to the given CMake project. +# Example: em_link_js_library(my_executable "lib1.js" "lib2.js") +# will result in emcc passing --js-library lib1.js --js-library lib2.js to +# the emscripten linker, as well as tracking the modification timestamp +# between the linked .js files and the main project, so that editing the .js +# file will cause the target project to be relinked. +function(em_link_js_library target) + em_add_link_deps(${target} "--js-library" ${ARGN}) +endfunction() + +# This function is identical to em_link_js_library(), except the .js files will +# be added with '--pre-js file.js' command line flag, which is generally used to +# add some preamble .js code to a generated output file. +function(em_link_pre_js target) + em_add_link_deps(${target} "--pre-js" ${ARGN}) +endfunction() + +# This function is identical to em_link_js_library(), except the .js files will +# be added with '--post-js file.js' command line flag, which is generally used +# to add some postamble .js code to a generated output file. +function(em_link_post_js target) + em_add_link_deps(${target} "--post-js" ${ARGN}) +endfunction() + +if (NOT DEFINED CMAKE_CROSSCOMPILING_EMULATOR) + find_program(NODE_JS_EXECUTABLE NAMES nodejs node) + if(NODE_JS_EXECUTABLE) + set(CMAKE_CROSSCOMPILING_EMULATOR "${NODE_JS_EXECUTABLE}" CACHE FILEPATH "Path to the emulator for the target system.") + endif() +endif() + +# No-op on CMAKE_CROSSCOMPILING_EMULATOR so older versions of cmake do not +# complain about unused CMake variable. +if (CMAKE_CROSSCOMPILING_EMULATOR) +endif() diff --git a/cmake/Toolchain-mingw32.cmake b/cmake/Toolchain-mingw32.cmake new file mode 100644 index 0000000..a8767fd --- /dev/null +++ b/cmake/Toolchain-mingw32.cmake @@ -0,0 +1,15 @@ +set(CMAKE_SYSTEM_NAME Windows) + +set(TRIPLE i686-w64-mingw32) + +set(CMAKE_C_COMPILER ${TRIPLE}-gcc) +set(CMAKE_CXX_COMPILER ${TRIPLE}-g++) +set(CMAKE_RC_COMPILER ${TRIPLE}-windres) + +set(CMAKE_FIND_ROOT_PATH /usr/${TRIPLE}) + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + +set(ENV{PKG_CONFIG_LIBDIR} /usr/${TRIPLE}/lib/pkgconfig) diff --git a/cmake/Toolchain-mingw64.cmake b/cmake/Toolchain-mingw64.cmake new file mode 100644 index 0000000..7f85f35 --- /dev/null +++ b/cmake/Toolchain-mingw64.cmake @@ -0,0 +1,15 @@ +set(CMAKE_SYSTEM_NAME Windows) + +set(TRIPLE x86_64-w64-mingw32) + +set(CMAKE_C_COMPILER ${TRIPLE}-gcc) +set(CMAKE_CXX_COMPILER ${TRIPLE}-g++) +set(CMAKE_RC_COMPILER ${TRIPLE}-windres) + +set(CMAKE_FIND_ROOT_PATH /usr/${TRIPLE}) + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + +set(ENV{PKG_CONFIG_LIBDIR} /usr/${TRIPLE}/lib/pkgconfig) diff --git a/cmake/c_flag_overrides.cmake b/cmake/c_flag_overrides.cmake new file mode 100644 index 0000000..7d33e9b --- /dev/null +++ b/cmake/c_flag_overrides.cmake @@ -0,0 +1,11 @@ +if(MSVC) + set(CMAKE_C_FLAGS_DEBUG_INIT "/D_DEBUG /MTd /Zi /Ob0 /Od /RTC1") + set(CMAKE_C_FLAGS_MINSIZEREL_INIT "/MT /O1 /Ob1 /D NDEBUG") + set(CMAKE_C_FLAGS_RELEASE_INIT "/MT /O2 /Ob2 /D NDEBUG") + set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "/MT /Zi /O2 /Ob1 /D NDEBUG") + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +endif() + +if(EMSCRIPTEN) + set(CMAKE_C_FLAGS_DEBUG_INIT "-g4") +endif() diff --git a/cmake/cxx_flag_overrides.cmake b/cmake/cxx_flag_overrides.cmake new file mode 100644 index 0000000..9c8d15f --- /dev/null +++ b/cmake/cxx_flag_overrides.cmake @@ -0,0 +1,10 @@ +if(MSVC) + set(CMAKE_CXX_FLAGS_DEBUG_INIT "/D_DEBUG /MTd /Zi /Ob0 /Od /RTC1") + set(CMAKE_CXX_FLAGS_MINSIZEREL_INIT "/MT /O1 /Ob1 /D NDEBUG") + set(CMAKE_CXX_FLAGS_RELEASE_INIT "/MT /O2 /Ob2 /D NDEBUG") + set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "/MT /Zi /O2 /Ob1 /D NDEBUG") +endif() + +if(EMSCRIPTEN) + set(CMAKE_CXX_FLAGS_DEBUG_INIT "-g4") +endif() diff --git a/developer_docs/IdLists_Entities_and_Remap.txt b/developer_docs/IdLists_Entities_and_Remap.txt new file mode 100644 index 0000000..5fe1791 --- /dev/null +++ b/developer_docs/IdLists_Entities_and_Remap.txt @@ -0,0 +1,49 @@ +Some notes about Entities, Entity IDs and the IdList structure +============================================================== +Sketch entities in SolveSpace are all of the same type without use of language +support for polymorphism. The entity class is defined in sketch.h. That class +contains an enum for each entity to define its type (line, arc, etc...) and some +other members that can be used to store different things depending on the entity +type. This means all entities are the same size, so some data may be reference +by pointers from the entity (font, extra points, etc...) + +Entities in a sketch are kept in a global array (IdList) referenced by a unique +Id (handle) and can be looked up by Id in log(n) time via binary search. In +order to use binary search the array must be kept in order sorted by Id. One +problem is that insertion takes O(n) time because half the list (on average) +must be shifted to make room for a new item. + +The IdList class is a template and is used for more than entities. + +EntityMap: +========== +Another important structure is the EntityMap and EntityKey defined in sketch.h +This is what allows SovleSpace to update groups when earlier groups in the +sketch are changed. If a rectangle is extruded to a box and items are +constrained to entities on that box, the user can go back to the sketch and +modify it. Entities can be added, modified an even deleted. So long as the +entities that are later used to build upon are kept the later extrude group will +pick up the changes from the 2D sketch and anything build on it will remain. + +The way this works is that each group has a member called remap, which is one of +these maps. This is where my understanding is fuzzy. At the end of Group.cpp is +a function called Group::CopyEntity() which is used to make new sketch entities +when a group is created. These are generally copies of entities in the previous +group, but there are exceptions. A point will be used to generate a line when +extruding a 2D sketch. A point will also be "copied" to a circle for a Lathe +group. For this reason, the entity key is derived by combining its previous key +with something often called the CopyNumber or just remap (unfortunate). + +When a group is regenerated (the first time, or after a previous one is +modified) entities are copied from the old group to the new one. For Step +Translating and Rotating there may be many copies, and the copy number is +literally N for the Nth copy except for the last one which gets an enum - it is +common to constrain the last item, so it gets a large unique number so that +constraints still refer to it if the number of copies changes. When an entity is +copied like this a new handle is created unless there is already an entity in +Remap that was created the same way. This is how constructions are preserved +across underlying changes. + +There are some hard limits used in the hash table for the remap mechanism which +limit the number of entities in a group (but not the global sketch). + diff --git a/developer_docs/Solver_Transforms.txt b/developer_docs/Solver_Transforms.txt new file mode 100644 index 0000000..98300f8 --- /dev/null +++ b/developer_docs/Solver_Transforms.txt @@ -0,0 +1,154 @@ + +CREATING NEW GROUPS in 3D +================================= + +New Transformed Entity Types: +----------------------------- +New construction tools may require new transformations of entities - Points, +Normals, and Faces. A number of "Types" exist to transform an entity to a new +location. The term "Type" refers to the type of transformation an entity was +created from, and should not be confused with the 3 kinds of Entity (there are +other entities but they rely on others of the 3 base types for position and +orientation information, so they don't directly transform). A list of +Types is at the top of the EntityBase class definition: + + enum class Type : uint32_t { + POINT_IN_3D = 2000, + POINT_IN_2D = 2001, + POINT_N_TRANS = 2010, + POINT_N_ROT_TRANS = 2011, + POINT_N_COPY = 2012, + POINT_N_ROT_AA = 2013, + POINT_N_ROT_AXIS_TRANS = 2014, + + NORMAL_IN_3D = 3000, + NORMAL_IN_2D = 3001, + NORMAL_N_COPY = 3010, + NORMAL_N_ROT = 3011, + NORMAL_N_ROT_AA = 3012, + + FACE_NORMAL_PT = 5000, + FACE_XPROD = 5001, + FACE_N_ROT_TRANS = 5002, + FACE_N_TRANS = 5003, + FACE_N_ROT_AA = 5004, + +Some of the point definitions with _N_ in the name are points defined by +N application of a transformation. The number of times a particular entity +is transformed is given in the member variable "timesApplied". The following +is a dectription of the various transformation Types. + +POINT_N_TRANS: Translates a point by a vector defined by param[0],param[1],param[2] + the vector is multiplied by timesApplied. + +POINT_N_ROT_TRANS: Rotates a point via quaternion param[3],param[4],param[5],param[6] + and then translates it by vector param[0],param[1],param[2] + +POINT_N_COPY: A non-transformed copy of a point - numeric copy? + +POINT_N_ROT_AA: A point rotated around point param[0],param[1],param[2] Where the + angle is given by param[3]*timesApplied (times 2?) and the axis + of rotation defined by param[4],param[5],param[6] + +POINT_N_ROT_AXIS_TRANS: Same as POINT_N_ROT_AA but after rotation, the point is + translated along the rotation axis by distance param[7]. + +NORMAL_N_COPY A non-transformed copy of a normal - numeric copy? + +NORMAL_N_ROT: A normal rotated by a quaternion defeined by param[0],param[1],param[2],param[3] + +NORMAL_N_ROT_AA A normal rotated by timesApplied*param[0] around the axis + specified by param[1],param[2],parma[3] + +FACE_N_ROT_TRANS A face rotated then translated. Rotation is defined by + quaternion param[3],param[4],param[5],param[6]. The translation + is defined by param[0],param[1],param[2]. + +FACE_N_TRANS: Translates a face by a vector defined by param[0],param[1],param[2] + faces are defined by a point and a normal, so this just moves the point. + +FACE_N_ROT_AA Face rotated about point param[0],param[1],param[2]. The axis + is param[4],param[5],param[6]. Angle is timesApplied*param[3]. + + +All entities are copied by the function Group::CopyEntity() which has a CopyAs +parameter to indicate which kind of copy is to be made. The mapping from CopyAs to +Entity Types can be found in that function. Most point types get copied the same +way depending on CopyAs. Several of the normals get copied to the same new +entity Type because they are unaffected by translation - you don't care if CopyAs +specified a translation with rotation or just a rotation, a normal is affected the +same in either case. The mapping from entity type to new entity type has to be +decoded from the cases and if-else logic in that function. + +It is important that a transformation be properly applied to all three of the +fundamental entities - points, normals, and surfaces. + +FUNCTIONS THAT MAY NEED TO BE EXTENDED when new entity types are defined: + +These functions have default cases, so they only need to be extended to return True: +EntityBase::IsPoint() +EntityBase::IsNormal() +EntityBAse::IsFace() + +For new normal transforms the following should be filled in: +Quaternion EntityBase::NormalGetNum() +void EntityBase::NormalForceTo(Quaternion q) +ExprQuaternion EntityBase::NormalGetExprs() + +For points: +Vector EntityBase::PointGetNum() +void EntityBase::PointForceTo(Vector p) +ExprVector EntityBase::PointGetExprs() + +For Faces: +ExprVector EntityBase::FaceGetNormalExprs() +Vector EntityBase::FaceGetNormalNum() +ExprVector EntityBase::FaceGetPointExprs() +Vector EntityBase::FaceGetPointNum() + +The basic model for these transformed entities is that the group containing them +will have parameters that define the transformed entity in terms of the one it +was copied from. For example, in PointGetNum() under the case POINT_N_TRANS we see: + + Vector trans = Vector::From(param[0], param[1], param[2]); + p = numPoint.Plus(trans.ScaledBy(timesApplied)); + break; + +This take the original "numPoint" and adds a vector created from 3 parameters and +multiplied by "timesApplied". This is used in the step-translating groups. It's also +used in extrude groups where there are constraints on the 3 parameters to keep the +vector perpendicular to the sketch it was extruded from. This function returns a +numerical version of the copied point, hence "GetNum" in the name. + +Another function PointGetExprs() returns the same coordinates of the point but +returns an expression vector. When the user applies a constraint to a point, this +function is called to get algebraic expressions for the point that are suitable for +use in the solver. Most points will not be constrained directly, so expressions +are not needed for them. It is also notable that the expressions are not part of +the entity itself. + +The ForceTo() functions are shortcuts for using the solver. They are passed the +desired location of a point (or orientation of a normal...) and have the opportunity +to back-calculate what the group parameters should be to place it there. This is +used for mouse dragging of copied entities. It is notable that the constraints will +still be applied afterward, but this is a good shortcut. + +When creating a new entity transformation, the first thing to do is define the +numerical copy (xxxGetNum). That should allow display. Dragging will not work +until ForceTo is implemented, and user constraints will not work until GetExprs() +is completed. Most of these functions have an assert that will fire if the required +new case is missing. This is may not be a complete list of everything you need to +make new entity transformations. + +One thing of note, parameters in both entities and groups are stored by their handle. +An entity can have up to 8 parameters to define how it is transformed from another +entity. By convention this array of parameter handles matches that of the group but +this is probably not strictly necessary. + +ADDING GROUP CONSTRAINTS: +The example above where a point is copied via POINT_N_TRANS for both EXTRUDE and +STEP TRANSLATING groups is a good example here. For the EXTRUDE case, two constraint +equations are added to keep the offset vector perpendicular to the sketch. These +equations are created in Group::GenerateEquations() and are nothing more than an +expression which is implicitly set equal to zero in the solver. + diff --git a/exposed/CDemo.c b/exposed/CDemo.c new file mode 100644 index 0000000..a5564b3 --- /dev/null +++ b/exposed/CDemo.c @@ -0,0 +1,302 @@ +/*----------------------------------------------------------------------------- + * Some sample code for slvs.dll. We draw some geometric entities, provide + * initial guesses for their positions, and then constrain them. The solver + * calculates their new positions, in order to satisfy the constraints. + * + * Copyright 2008-2013 Jonathan Westhues. + *---------------------------------------------------------------------------*/ +#ifdef WIN32 +# include +#endif +#include +#include +#include +#include + +#include + +static Slvs_System sys; + +static void *CheckMalloc(size_t n) +{ + void *r = malloc(n); + if(!r) { + printf("out of memory!\n"); + exit(-1); + } + return r; +} + +void ExampleStateful() +{ + Slvs_hGroup g = 1; + Slvs_Entity wp = Slvs_AddBase2D(g); + Slvs_Entity p1 = Slvs_AddPoint2D(g, 0.0, 10.0, wp); + Slvs_Entity p2 = Slvs_AddPoint2D(g, 5.0, 20.0, wp); + Slvs_AddLine2D(g, p1, p2, wp); + + // double w, vx, vy, vz; + // Slvs_MakeQuaternion(1, 0, 0, 0, 1, 0, &w, &vx, &vy, &vz); + // Slvs_Entity n = Slvs_AddNormal3D(g, w, vx, vy, vz); + // Slvs_Entity center = Slvs_AddPoint2D(g, 5.0, 5.0, wp); + // Slvs_Entity radius = Slvs_AddDistance(g, 50.0, wp); + // Slvs_Entity c1 = Slvs_AddCircle(g, n, center, radius, wp); + + Slvs_Vertical(g, p1, wp, p2); + Slvs_SolveResult res = Slvs_SolveSketch(g, NULL); + printf("res: %i\n", res.result); + printf("dof: %i\n", res.dof); + double p1x = Slvs_GetParamValue(p1.param[0]); + double p1y = Slvs_GetParamValue(p1.param[1]); + double p2x = Slvs_GetParamValue(p2.param[0]); + double p2y = Slvs_GetParamValue(p2.param[1]); + printf("p1x:%.3f\n", p1x); + printf("p1y:%.3f\n", p1y); + printf("p2x:%.3f\n", p2x); + printf("p2y:%.3f\n", p2y); +} + +/*----------------------------------------------------------------------------- + * An example of a constraint in 3d. We create a single group, with some + * entities and constraints. + *---------------------------------------------------------------------------*/ +void Example3d() +{ + /* This will contain a single group, which will arbitrarily number 1. */ + Slvs_hGroup g = 1; + + /* A point, initially at (x y z) = (10 10 10) */ + sys.param[sys.params++] = Slvs_MakeParam(1, g, 10.0); + sys.param[sys.params++] = Slvs_MakeParam(2, g, 10.0); + sys.param[sys.params++] = Slvs_MakeParam(3, g, 10.0); + sys.entity[sys.entities++] = Slvs_MakePoint3d(101, g, 1, 2, 3); + /* and a second point at (20 20 20) */ + sys.param[sys.params++] = Slvs_MakeParam(4, g, 20.0); + sys.param[sys.params++] = Slvs_MakeParam(5, g, 20.0); + sys.param[sys.params++] = Slvs_MakeParam(6, g, 20.0); + sys.entity[sys.entities++] = Slvs_MakePoint3d(102, g, 4, 5, 6); + /* and a line segment connecting them. */ + sys.entity[sys.entities++] = Slvs_MakeLineSegment(200, g, + SLVS_FREE_IN_3D, 101, 102); + + /* The distance between the points should be 30.0 units. */ + sys.constraint[sys.constraints++] = Slvs_MakeConstraint( + 1, g, + SLVS_C_PT_PT_DISTANCE, + SLVS_FREE_IN_3D, + 30.0, + 101, 102, 0, 0); + + /* Let's tell the solver to keep the second point as close to constant + * as possible, instead moving the first point. */ + sys.dragged[sys.ndragged++] = 4; + sys.dragged[sys.ndragged++] = 5; + sys.dragged[sys.ndragged++] = 6; + + /* Now that we have written our system, we solve. */ + Slvs_Solve(&sys, g); + + if(sys.result == SLVS_RESULT_OKAY) { + printf("okay; now at (%.3f %.3f %.3f)\n" + " (%.3f %.3f %.3f)\n", + sys.param[0].val, sys.param[1].val, sys.param[2].val, + sys.param[3].val, sys.param[4].val, sys.param[5].val); + printf("%d DOF\n", sys.dof); + } else { + printf("solve failed"); + } +} + +/*----------------------------------------------------------------------------- + * An example of a constraint in 2d. In our first group, we create a workplane + * along the reference frame's xy plane. In a second group, we create some + * entities in that group and dimension them. + *---------------------------------------------------------------------------*/ +void Example2d() +{ + Slvs_hGroup g; + double qw, qx, qy, qz; + + g = 1; + /* First, we create our workplane. Its origin corresponds to the origin + * of our base frame (x y z) = (0 0 0) */ + sys.param[sys.params++] = Slvs_MakeParam(1, g, 0.0); + sys.param[sys.params++] = Slvs_MakeParam(2, g, 0.0); + sys.param[sys.params++] = Slvs_MakeParam(3, g, 0.0); + sys.entity[sys.entities++] = Slvs_MakePoint3d(101, g, 1, 2, 3); + /* and it is parallel to the xy plane, so it has basis vectors (1 0 0) + * and (0 1 0). */ + Slvs_MakeQuaternion(1, 0, 0, + 0, 1, 0, &qw, &qx, &qy, &qz); + sys.param[sys.params++] = Slvs_MakeParam(4, g, qw); + sys.param[sys.params++] = Slvs_MakeParam(5, g, qx); + sys.param[sys.params++] = Slvs_MakeParam(6, g, qy); + sys.param[sys.params++] = Slvs_MakeParam(7, g, qz); + sys.entity[sys.entities++] = Slvs_MakeNormal3d(102, g, 4, 5, 6, 7); + + sys.entity[sys.entities++] = Slvs_MakeWorkplane(200, g, 101, 102); + + /* Now create a second group. We'll solve group 2, while leaving group 1 + * constant; so the workplane that we've created will be locked down, + * and the solver can't move it. */ + g = 2; + /* These points are represented by their coordinates (u v) within the + * workplane, so they need only two parameters each. */ + sys.param[sys.params++] = Slvs_MakeParam(11, g, 10.0); + sys.param[sys.params++] = Slvs_MakeParam(12, g, 20.0); + sys.entity[sys.entities++] = Slvs_MakePoint2d(301, g, 200, 11, 12); + + sys.param[sys.params++] = Slvs_MakeParam(13, g, 20.0); + sys.param[sys.params++] = Slvs_MakeParam(14, g, 10.0); + sys.entity[sys.entities++] = Slvs_MakePoint2d(302, g, 200, 13, 14); + + /* And we create a line segment with those endpoints. */ + sys.entity[sys.entities++] = Slvs_MakeLineSegment(400, g, + 200, 301, 302); + + /* Now three more points. */ + sys.param[sys.params++] = Slvs_MakeParam(15, g, 100.0); + sys.param[sys.params++] = Slvs_MakeParam(16, g, 120.0); + sys.entity[sys.entities++] = Slvs_MakePoint2d(303, g, 200, 15, 16); + + sys.param[sys.params++] = Slvs_MakeParam(17, g, 120.0); + sys.param[sys.params++] = Slvs_MakeParam(18, g, 110.0); + sys.entity[sys.entities++] = Slvs_MakePoint2d(304, g, 200, 17, 18); + + sys.param[sys.params++] = Slvs_MakeParam(19, g, 115.0); + sys.param[sys.params++] = Slvs_MakeParam(20, g, 115.0); + sys.entity[sys.entities++] = Slvs_MakePoint2d(305, g, 200, 19, 20); + + /* And arc, centered at point 303, starting at point 304, ending at + * point 305. */ + sys.entity[sys.entities++] = Slvs_MakeArcOfCircle(401, g, 200, 102, + 303, 304, 305); + + /* Now one more point, and a distance */ + sys.param[sys.params++] = Slvs_MakeParam(21, g, 200.0); + sys.param[sys.params++] = Slvs_MakeParam(22, g, 200.0); + sys.entity[sys.entities++] = Slvs_MakePoint2d(306, g, 200, 21, 22); + + sys.param[sys.params++] = Slvs_MakeParam(23, g, 30.0); + sys.entity[sys.entities++] = Slvs_MakeDistance(307, g, 200, 23); + + /* And a complete circle, centered at point 306 with radius equal to + * distance 307. The normal is 102, the same as our workplane. */ + sys.entity[sys.entities++] = Slvs_MakeCircle(402, g, 200, + 306, 102, 307); + + + /* The length of our line segment is 30.0 units. */ + sys.constraint[sys.constraints++] = Slvs_MakeConstraint( + 1, g, + SLVS_C_PT_PT_DISTANCE, + 200, + 30.0, + 301, 302, 0, 0); + + /* And the distance from our line segment to the origin is 10.0 units. */ + sys.constraint[sys.constraints++] = Slvs_MakeConstraint( + 2, g, + SLVS_C_PT_LINE_DISTANCE, + 200, + 10.0, + 101, 0, 400, 0); + /* And the line segment is vertical. */ + sys.constraint[sys.constraints++] = Slvs_MakeConstraint( + 3, g, + SLVS_C_VERTICAL, + 200, + 0.0, + 0, 0, 400, 0); + /* And the distance from one endpoint to the origin is 15.0 units. */ + sys.constraint[sys.constraints++] = Slvs_MakeConstraint( + 4, g, + SLVS_C_PT_PT_DISTANCE, + 200, + 15.0, + 301, 101, 0, 0); +#if 0 + /* And same for the other endpoint; so if you add this constraint then + * the sketch is overconstrained and will signal an error. */ + sys.constraint[sys.constraints++] = Slvs_MakeConstraint( + 5, g, + SLVS_C_PT_PT_DISTANCE, + 200, + 18.0, + 302, 101, 0, 0); +#endif /* 0 */ + + /* The arc and the circle have equal radius. */ + sys.constraint[sys.constraints++] = Slvs_MakeConstraint( + 6, g, + SLVS_C_EQUAL_RADIUS, + 200, + 0.0, + 0, 0, 401, 402); + /* The arc has radius 17.0 units. */ + sys.constraint[sys.constraints++] = Slvs_MakeConstraint( + 7, g, + SLVS_C_DIAMETER, + 200, + 17.0*2, + 0, 0, 401, 0); + + /* If the solver fails, then ask it to report which constraints caused + * the problem. */ + sys.calculateFaileds = 1; + + /* And solve. */ + Slvs_Solve(&sys, g); + + if(sys.result == SLVS_RESULT_OKAY) { + printf("solved okay\n"); + printf("line from (%.3f %.3f) to (%.3f %.3f)\n", + sys.param[7].val, sys.param[8].val, + sys.param[9].val, sys.param[10].val); + + printf("arc center (%.3f %.3f) start (%.3f %.3f) finish (%.3f %.3f)\n", + sys.param[11].val, sys.param[12].val, + sys.param[13].val, sys.param[14].val, + sys.param[15].val, sys.param[16].val); + + printf("circle center (%.3f %.3f) radius %.3f\n", + sys.param[17].val, sys.param[18].val, + sys.param[19].val); + printf("%d DOF\n", sys.dof); + } else { + int i; + printf("solve failed: problematic constraints are:"); + for(i = 0; i < sys.faileds; i++) { + printf(" %d", sys.failed[i]); + } + printf("\n"); + if(sys.result == SLVS_RESULT_INCONSISTENT) { + printf("system inconsistent\n"); + } else { + printf("system nonconvergent\n"); + } + } +} + +int main() +{ + ExampleStateful(); + + sys.param = CheckMalloc(50*sizeof(sys.param[0])); + sys.entity = CheckMalloc(50*sizeof(sys.entity[0])); + sys.constraint = CheckMalloc(50*sizeof(sys.constraint[0])); + sys.dragged = CheckMalloc(50*sizeof(sys.dragged[0])); + + sys.failed = CheckMalloc(50*sizeof(sys.failed[0])); + sys.faileds = 50; + + Example3d(); + sys.params = sys.constraints = sys.entities = sys.ndragged = 0; + for(;;) { + Example2d(); + sys.params = sys.constraints = sys.entities = sys.ndragged = 0; + break; + } + return 0; +} + diff --git a/exposed/CMakeLists.txt b/exposed/CMakeLists.txt new file mode 100644 index 0000000..f0bce91 --- /dev/null +++ b/exposed/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(CDemo + CDemo.c) + +target_link_libraries(CDemo PRIVATE + slvs) + +if(EMSCRIPTEN) + target_link_options(CDemo PRIVATE + "SHELL:-s TOTAL_MEMORY=134217728") + set_target_properties(CDemo PROPERTIES + SUFFIX ".html") +endif() diff --git a/exposed/DOC.txt b/exposed/DOC.txt new file mode 100644 index 0000000..f64f3ac --- /dev/null +++ b/exposed/DOC.txt @@ -0,0 +1,467 @@ + +INTRODUCTION +============ + +A sketch in SolveSpace consists of three basic elements: parameters, +entities, and constraints. + +A parameter (Slvs_Param) is a single real number, represented internally +by a double-precision floating point variable. The parameters are unknown +variables that the solver modifies in order to satisfy the constraints. + +An entity (Slvs_Entity) is a geometric thing, like a point or a line +segment or a circle. Entities are defined in terms of parameters, +and in terms of other entities. For example, a point in three-space +is represented by three parameters, corresponding to its x, y, and z +coordinates in our base coordinate frame. A line segment is represented +by two point entities, corresponding to its endpoints. + +A constraint (Slvs_Constraint) is a geometric property of an entity, +or a relationship among multiple entities. For example, a point-point +distance constraint will set the distance between two point entities. + +Parameters, entities, and constraints are typically referenced by their +handles (Slvs_hParam, Slvs_hEntity, Slvs_hConstraint). These handles are +32-bit integer values starting from 1. The zero handle is reserved. Each +object has a unique handle within its type (but it's acceptable, for +example to have a constraint with an Slvs_hConstraint of 7, and also to +have an entity with an Slvs_hEntity of 7). The use of handles instead +of pointers helps to avoid memory corruption. + +Entities and constraints are assigned into groups. A group is a set of +entities and constraints that is solved simultaneously. In a parametric +CAD system, a single group would typically correspond to a single sketch. +Constraints within a group may refer to entities outside that group, +but only the entities within that group will be modified by the solver. + +Consider point A in group 1, and point B in group 2. We have a constraint +in group 2 that makes the points coincident. When we solve group 2, the +solver is allowed to move point B to place it on top of point A. It is +not allowed to move point A to put it on top of point B, because point +A is outside the group being solved. + +This corresponds to the typical structure of a parametric CAD system. In a +later sketch, we may constrain our entities against existing geometry from +earlier sketches. The constraints will move the entities in our current +sketch, but will not change the geometry from the earlier sketches. + +To use the solver, we first define a set of parameters, entities, and +constraints. We provide an initial guess for each parameter; this is +necessary to achieve convergence, and also determines which solution +gets chosen when (finitely many) multiple solutions exist. Typically, +these initial guesses are provided by the initial configuration in which +the user drew the entities before constraining them. + +We then run the solver for a given group. The entities within that group +are modified in an attempt to satisfy the constraints. + +After running the solver, there are three possible outcomes: + + * All constraints were satisfied to within our numerical + tolerance (i.e., success). The result is equal to SLVS_RESULT_OKAY, + and the parameters in param[] have been updated. + + * The solver can prove that two constraints are inconsistent (for + example, if a line with nonzero length is constrained both + horizontal and vertical). In that case, a list of inconsistent + constraints is generated in failed[]. + + * The solver cannot prove that two constraints are inconsistent, but + it cannot find a solution. In that case, the list of unsatisfied + constraints is generated in failed[]. + + +TYPES OF ENTITIES +================= + +SLVS_E_POINT_IN_3D + + A point in 3d. Defined by three parameters: + + param[0] the point's x coordinate + param[1] y + param[1] z + + +SLVS_E_POINT_IN_2D + + A point within a workplane. Defined by the workplane + + wrkpl + + and by two parameters + + param[0] the point's u coordinate + param[1] v + + within the coordinate system of the workplane. For example, if the + workplane is the zx plane, then u = z and v = x. If the workplane is + parallel to the zx plane, but translated so that the workplane's + origin is (3, 4, 5), then u = z - 5 and v = x - 3. + + +SLVS_E_NORMAL_IN_3D + + A normal. In SolveSpace, "normals" represent a 3x3 rotation matrix + from our base coordinate system to a new frame. Defined by the + unit quaternion + + param[0] w + param[1] x + param[2] y + param[3] z + + where the quaternion is given by w + x*i + y*j + z*k. + + It is useful to think of this quaternion as representing a plane + through the origin. This plane has three associated vectors: basis + vectors U, V that lie within the plane, and normal N that is + perpendicular to it. This means that + + [ U V N ]' + + defines a 3x3 rotation matrix. So U, V, and N all have unit length, + and are orthogonal so that + + U cross V = N + V cross N = U + N cross U = V + + Convenience functions (Slvs_Quaternion*) are provided to convert + between this representation as vectors U, V, N and the unit + quaternion. + + A unit quaternion has only 3 degrees of freedom, but is specified in + terms of 4 parameters. An extra constraint is therefore generated + implicitly, that + + w^2 + x^2 + y^2 + z^2 = 1 + + +SLVS_E_NORMAL_IN_2D + + A normal within a workplane. This is identical to the workplane's + normal, so it is simply defined by + + wrkpl + + This entity type is used, for example, to define a circle that lies + within a workplane. The circle's normal is the same as the workplane's + normal, so we can use an SLVS_E_NORMAL_IN_2D to copy the workplane's + normal. + + +SLVS_E_DISTANCE + + A distance. This entity is used to define the radius of a circle, by + a single parameter + + param[0] r + + +SLVS_E_WORKPLANE + + An oriented plane, somewhere in 3d. This entity therefore has 6 + degrees of freedom: three translational, and three rotational. It is + specified in terms of its origin + + point[0] origin + + and a normal + + normal + + The normal describes three vectors U, V, N, as discussed in the + documentation for SLVS_E_NORMAL_IN_3D. The plane is therefore given + by the equation + + p = origin + s*U + t*V + + for any scalar s and t. + + +SLVS_E_LINE_SEGMENT + + A line segment between two endpoints + + point[0] + point[1] + + +SLVS_E_CUBIC + + A nonrational cubic Bezier segment + + point[0] starting point P0 + point[1] control point P1 + point[2] control point P2 + point[3] ending point P3 + + The curve then has equation + + p(t) = P0*(1 - t)^3 + 3*P1*(1 - t)^2*t + 3*P2*(1 - t)*t^2 + P3*t^3 + + as t goes from 0 to 1. + + +SLVS_E_CIRCLE + + A complete circle. The circle lies within a plane with normal + + normal + + The circle is centered at + + point[0] + + The circle's radius is + + distance + + +SLVS_E_ARC_OF_CIRCLE + + An arc of a circle. An arc must always lie within a workplane; it + cannot be free in 3d. So it is specified with a workplane + + wrkpl + + It is then defined by three points + + point[0] center of the circle + point[1] beginning of the arc + point[2] end of the arc + + and its normal + + normal identical to the normal of the workplane + + The arc runs counter-clockwise from its beginning to its end (with + the workplane's normal pointing towards the viewer). If the beginning + and end of the arc are coincident, then the arc is considered to + represent a full circle. + + This representation has an extra degree of freedom. An extra + constraint is therefore generated implicitly, so that + + distance(center, beginning) = distance(center, end) + + +TYPES OF CONSTRAINTS +==================== + +Many constraints can apply either in 3d, or in a workplane. This is +determined by the wrkpl member of the constraint. If that member is set +to SLVS_FREE_IN_3D, then the constraint applies in 3d. If that member +is set equal to a workplane, the constraint applies projected into that +workplane. (For example, a constraint on the distance between two points +actually applies to the projected distance). + +Constraints that may be used in 3d or projected into a workplane are +marked with a single star (*). Constraints that must always be used with +a workplane are marked with a double star (**). Constraints that ignore +the wrkpl member are marked with no star. + +SLVS_C_PT_PT_DISTANCE* + + The distance between points ptA and ptB is equal to valA. This is an + unsigned distance, so valA must always be positive. + +SLVS_C_PROJ_PT_DISTANCE + + The distance between points ptA and ptB, as projected along the line + or normal entityA, is equal to valA. This is a signed distance. + +SLVS_C_POINTS_COINCIDENT* + + Points ptA and ptB are coincident (i.e., exactly on top of each + other). + +SLVS_C_PT_PLANE_DISTANCE + + The distance from point ptA to workplane entityA is equal to + valA. This is a signed distance; positive versus negative valA + correspond to a point that is above vs. below the plane. + +SLVS_C_PT_LINE_DISTANCE* + + The distance from point ptA to line segment entityA is equal to valA. + + If the constraint is projected, then valA is a signed distance; + positive versus negative valA correspond to a point that is above + vs. below the line. + + If the constraint applies in 3d, then valA must always be positive. + +SLVS_C_PT_IN_PLANE + + The point ptA lies in plane entityA. + +SLVS_C_PT_ON_LINE* + + The point ptA lies on the line entityA. + + Note that this constraint removes one degree of freedom when projected + in to the plane, but two degrees of freedom in 3d. + +SLVS_C_EQUAL_LENGTH_LINES* + + The lines entityA and entityB have equal length. + +SLVS_C_LENGTH_RATIO* + + The length of line entityA divided by the length of line entityB is + equal to valA. + +SLVS_C_LENGTH_DIFFERENCE* + + The lengths of line entityA and line entityB differ by valA. + +SLVS_C_EQ_LEN_PT_LINE_D* + + The length of the line entityA is equal to the distance from point + ptA to line entityB. + +SLVS_C_EQ_PT_LN_DISTANCES* + + The distance from the line entityA to the point ptA is equal to the + distance from the line entityB to the point ptB. + +SLVS_C_EQUAL_ANGLE* + + The angle between lines entityA and entityB is equal to the angle + between lines entityC and entityD. + + If other is true, then the angles are supplementary (i.e., theta1 = + 180 - theta2) instead of equal. + +SLVS_C_EQUAL_LINE_ARC_LEN* + + The length of the line entityA is equal to the length of the circular + arc entityB. + +SLVS_C_SYMMETRIC* + + The points ptA and ptB are symmetric about the plane entityA. This + means that they are on opposite sides of the plane and at equal + distances from the plane, and that the line connecting ptA and ptB + is normal to the plane. + +SLVS_C_SYMMETRIC_HORIZ +SLVS_C_SYMMETRIC_VERT** + + The points ptA and ptB are symmetric about the horizontal or vertical + axis of the specified workplane. + +SLVS_C_SYMMETRIC_LINE** + + The points ptA and ptB are symmetric about the line entityA. + +SLVS_C_AT_MIDPOINT* + + The point ptA lies at the midpoint of the line entityA. + +SLVS_C_HORIZONTAL +SLVS_C_VERTICAL** + + The line connecting points ptA and ptB is horizontal or vertical. Or, + the line segment entityA is horizontal or vertical. If points are + specified then the line segment should be left zero, and if a line + is specified then the points should be left zero. + +SLVS_C_DIAMETER + + The diameter of circle or arc entityA is equal to valA. + +SLVS_C_PT_ON_CIRCLE + + The point ptA lies on the right cylinder obtained by extruding circle + or arc entityA normal to its plane. + +SLVS_C_SAME_ORIENTATION + + The normals entityA and entityB describe identical rotations. This + constraint therefore restricts three degrees of freedom. + +SLVS_C_ANGLE* + + The angle between lines entityA and entityB is equal to valA, where + valA is specified in degrees. This constraint equation is written + in the form + + (A dot B)/(|A||B|) = cos(valA) + + where A and B are vectors in the directions of lines A and B. This + equation does not specify the angle unambiguously; for example, + note that valA = +/- 90 degrees will produce the same equation. + + If other is true, then the constraint is instead that + + (A dot B)/(|A||B|) = -cos(valA) + +SLVS_C_PERPENDICULAR* + + Identical to SLVS_C_ANGLE with valA = 90 degrees. + +SLVS_C_PARALLEL* + + Lines entityA and entityB are parallel. + + Note that this constraint removes one degree of freedom when projected + in to the plane, but two degrees of freedom in 3d. + +SLVS_C_ARC_LINE_TANGENT** + + The arc entityA is tangent to the line entityB. If other is false, + then the arc is tangent at its beginning (point[1]). If other is true, + then the arc is tangent at its end (point[2]). + +SLVS_C_CUBIC_LINE_TANGENT* + + The cubic entityA is tangent to the line entityB. The variable + other indicates: + + if false: the cubic is tangent at its beginning + if true: the cubic is tangent at its end + + The beginning of the cubic is point[0], and the end is point[3]. + +SLVS_C_CURVE_CURVE_TANGENT** + + The two entities entityA and entityB are tangent. These entities can + each be either an arc or a cubic, in any combination. The flags + other and other2 indicate which endpoint of the curve is tangent, + for entityA and entityB respectively: + + if false: the entity is tangent at its beginning + if true: the entity is tangent at its end + + For cubics, point[0] is the beginning, and point[3] is the end. For + arcs, point[1] is the beginning, and point[2] is the end. + +SLVS_C_EQUAL_RADIUS + + The circles or arcs entityA and entityB have equal radius. + +SLVS_C_WHERE_DRAGGED* + + The point ptA is locked at its initial numerical guess, and cannot + be moved. This constrains two degrees of freedom in a workplane, + and three in free space. It's therefore possible for this constraint + to overconstrain the sketch, for example if it's applied to a point + with one remaining degree of freedom. + + +USING THE SOLVER +================ + +The solver is provided as a DLL, and will be usable with most +Windows-based development tools. Examples are provided: + + in C/C++ - CDemo.c + + in VB.NET - VbDemo.vb + + +Copyright 2009-2013 Jonathan Westhues. + diff --git a/exposed/VbDemo.vb b/exposed/VbDemo.vb new file mode 100644 index 0000000..708de93 --- /dev/null +++ b/exposed/VbDemo.vb @@ -0,0 +1,1257 @@ +'----------------------------------------------------------------------------- +' Some sample code for slvs.dll. We draw some geometric entities, provide +' initial guesses for their positions, and then constrain them. The solver +' calculates their new positions, in order to satisfy the constraints. +' +' The library is distributed as a DLL, but the functions are designed to +' be usable from .net languages through a P/Invoke. This file contains an +' example of that process, and a wrapper class around those P/Invoke'd +' functions that you may wish to use a starting point in your own +' application. +' +' Copyright 2008-2013 Jonathan Westhues. +'----------------------------------------------------------------------------- + +Imports System.Runtime.InteropServices + +Module VbDemo + + ' Call our example functions, which set up some kind of sketch, solve + ' it, and then print the result. + Sub Main() + Console.WriteLine("EXAMPLE IN 3d (by objects):") + Example3dWithObjects() + Console.WriteLine("") + + Console.WriteLine("EXAMPLE IN 2d (by objects):") + Example2dWithObjects() + Console.WriteLine("") + + Console.WriteLine("EXAMPLE IN 3d (by handles):") + Example3dWithHandles() + Console.WriteLine("") + + Console.WriteLine("EXAMPLE IN 2d (by handles):") + Example2dWithHandles() + Console.WriteLine("") + End Sub + + + ''''''''''''''''''''''''''''''' + ' This is the simplest way to use the library. A set of wrapper + ' classes allow us to represent entities (e.g., lines and points) + ' as .net objects. So we create an Slvs object, which will contain + ' the entire sketch, with all the entities and constraints. + ' + ' We then create entity objects (for example, points and lines) + ' associated with that sketch, indicating the initial positions of + ' those entities and any hierarchical relationships among them (e.g., + ' defining a line entity in terms of endpoint entities). We also add + ' constraints associated with those entities. + ' + ' Finally, we solve, and print the new positions of the entities. If the + ' solution succeeded, then the entities will satisfy the constraints. If + ' not, then the solver will suggest problematic constraints that, if + ' removed, would render the sketch solvable. + + Sub Example3dWithObjects() + Dim g As UInteger + Dim slv As New Slvs + + ' This will contain a single group, which will arbitrarily number 1. + g = 1 + + Dim p1, p2 As Slvs.Point3d + ' A point, initially at (x y z) = (10 10 10) + p1 = slv.NewPoint3d(g, 10.0, 10.0, 10.0) + ' and a second point at (20 20 20) + p2 = slv.NewPoint3d(g, 20.0, 20.0, 20.0) + + Dim ln As Slvs.LineSegment + ' and a line segment connecting them. + ln = slv.NewLineSegment(g, slv.FreeIn3d(), p1, p2) + + ' The distance between the points should be 30.0 units. + slv.AddConstraint(1, g, Slvs.SLVS_C_PT_PT_DISTANCE, _ + slv.FreeIn3d(), 30.0, p1, p2, Nothing, Nothing) + + ' Let's tell the solver to keep the second point as close to constant + ' as possible, instead moving the first point. + slv.Solve(g, p2, True) + + If (slv.GetResult() = Slvs.SLVS_RESULT_OKAY) Then + ' We call the GetX(), GetY(), and GetZ() functions to see + ' where the solver moved our points to. + Console.WriteLine(String.Format( _ + "okay; now at ({0:F3}, {1:F3}, {2:F3})", _ + p1.GetX(), p1.GetY(), p1.GetZ())) + Console.WriteLine(String.Format( _ + " ({0:F3}, {1:F3}, {2:F3})", _ + p2.GetX(), p2.GetY(), p2.GetZ())) + Console.WriteLine(slv.GetDof().ToString() + " DOF") + Else + Console.WriteLine("solve failed") + End If + End Sub + + Sub Example2dWithObjects() + Dim g As UInteger + Dim slv As New Slvs + + g = 1 + + ' First, we create our workplane. Its origin corresponds to the origin + ' of our base frame (x y z) = (0 0 0) + Dim origin As Slvs.Point3d + origin = slv.NewPoint3d(g, 0.0, 0.0, 0.0) + ' and it is parallel to the xy plane, so it has basis vectors (1 0 0) + ' and (0 1 0). + Dim normal As Slvs.Normal3d + normal = slv.NewNormal3d(g, 1.0, 0.0, 0.0, _ + 0.0, 1.0, 0.0) + + Dim wrkpl As Slvs.Workplane + wrkpl = slv.NewWorkplane(g, origin, normal) + + ' Now create a second group. We'll solve group 2, while leaving group 1 + ' constant; so the workplane that we've created will be locked down, + ' and the solver can't move it. + g = 2 + ' These points are represented by their coordinates (u v) within the + ' workplane, so they need only two parameters each. + Dim pl1, pl2 As Slvs.Point2d + pl1 = slv.NewPoint2d(g, wrkpl, 10.0, 20.0) + pl2 = slv.NewPoint2d(g, wrkpl, 20.0, 10.0) + + ' And we create a line segment with those endpoints. + Dim ln As Slvs.LineSegment + ln = slv.NewLineSegment(g, wrkpl, pl1, pl2) + + ' Now three more points. + Dim pc, ps, pf As Slvs.Point2d + pc = slv.NewPoint2d(g, wrkpl, 100.0, 120.0) + ps = slv.NewPoint2d(g, wrkpl, 120.0, 110.0) + pf = slv.NewPoint2d(g, wrkpl, 115.0, 115.0) + + ' And arc, centered at point pc, starting at point ps, ending at + ' point pf. + Dim arc As Slvs.ArcOfCircle + arc = slv.NewArcOfCircle(g, wrkpl, normal, pc, ps, pf) + + ' Now one more point, and a distance + Dim pcc As Slvs.Point2d + pcc = slv.NewPoint2d(g, wrkpl, 200.0, 200.0) + Dim r As Slvs.Distance + r = slv.NewDistance(g, wrkpl, 30.0) + + ' And a complete circle, centered at point pcc with radius equal to + ' distance r. The normal is the same as for our workplane. + Dim circle As Slvs.Circle + circle = slv.NewCircle(g, wrkpl, pcc, normal, r) + + ' The length of our line segment is 30.0 units. + slv.AddConstraint(1, g, Slvs.SLVS_C_PT_PT_DISTANCE, _ + wrkpl, 30.0, pl1, pl2, Nothing, Nothing) + + ' And the distance from our line segment to the origin is 10.0 units. + slv.AddConstraint(2, g, Slvs.SLVS_C_PT_LINE_DISTANCE, _ + wrkpl, 10.0, origin, Nothing, ln, Nothing) + + ' And the line segment is vertical. + slv.AddConstraint(3, g, Slvs.SLVS_C_VERTICAL, _ + wrkpl, 0.0, Nothing, Nothing, ln, Nothing) + + ' And the distance from one endpoint to the origin is 15.0 units. + slv.AddConstraint(4, g, Slvs.SLVS_C_PT_PT_DISTANCE, _ + wrkpl, 15.0, pl1, origin, Nothing, Nothing) + + ' And same for the other endpoint; so if you add this constraint then + ' the sketch is overconstrained and will signal an error. + 'slv.AddConstraint(5, g, Slvs.SLVS_C_PT_PT_DISTANCE, _ + ' wrkpl, 18.0, pl2, origin, Nothing, Nothing) + + ' The arc and the circle have equal radius. + slv.AddConstraint(6, g, Slvs.SLVS_C_EQUAL_RADIUS, _ + wrkpl, 0.0, Nothing, Nothing, arc, circle) + + ' The arc has radius 17.0 units. + slv.AddConstraint(7, g, Slvs.SLVS_C_DIAMETER, _ + wrkpl, 2 * 17.0, Nothing, Nothing, arc, Nothing) + + ' If the solver fails, then ask it to report which constraints caused + ' the problem. + slv.Solve(g, True) + + If (slv.GetResult() = Slvs.SLVS_RESULT_OKAY) Then + Console.WriteLine("solved okay") + ' We call the GetU(), GetV(), and GetDistance() functions to + ' see where the solver moved our points and distances to. + Console.WriteLine(String.Format( _ + "line from ({0:F3} {1:F3}) to ({2:F3} {3:F3})", _ + pl1.GetU(), pl1.GetV(), _ + pl2.GetU(), pl2.GetV())) + Console.WriteLine(String.Format( _ + "arc center ({0:F3} {1:F3}) start ({2:F3} {3:F3}) " + _ + "finish ({4:F3} {5:F3})", _ + pc.GetU(), pc.GetV(), _ + ps.GetU(), ps.GetV(), _ + pf.GetU(), pf.GetV())) + Console.WriteLine(String.Format( _ + "circle center ({0:F3} {1:F3}) radius {2:F3}", _ + pcc.GetU(), pcc.GetV(), _ + r.GetDistance())) + + Console.WriteLine(slv.GetDof().ToString() + " DOF") + Else + Console.Write("solve failed; problematic constraints are:") + Dim t As UInteger + For Each t In slv.GetFaileds() + Console.Write(" " + t.ToString()) + Next + Console.WriteLine("") + If (slv.GetResult() = Slvs.SLVS_RESULT_INCONSISTENT) Then + Console.WriteLine("system inconsistent") + Else + Console.WriteLine("system nonconvergent") + End If + End If + + End Sub + + + ''''''''''''''''''''''''''''''' + ' This is a lower-level way to use the library. Internally, the library + ' represents parameters, entities, and constraints by integer handles. + ' Here, those handles are assigned manually, and not by the wrapper + ' classes. + + Sub Example3dWithHandles() + Dim g As UInteger + Dim slv As New Slvs + + ' This will contain a single group, which will arbitrarily number 1. + g = 1 + + ' A point, initially at (x y z) = (10 10 10) + slv.AddParam(1, g, 10.0) + slv.AddParam(2, g, 10.0) + slv.AddParam(3, g, 10.0) + slv.AddPoint3d(101, g, 1, 2, 3) + + ' and a second point at (20 20 20) + slv.AddParam(4, g, 20.0) + slv.AddParam(5, g, 20.0) + slv.AddParam(6, g, 20.0) + slv.AddPoint3d(102, g, 4, 5, 6) + + ' and a line segment connecting them. + slv.AddLineSegment(200, g, Slvs.SLVS_FREE_IN_3D, 101, 102) + + ' The distance between the points should be 30.0 units. + slv.AddConstraint(1, g, Slvs.SLVS_C_PT_PT_DISTANCE, _ + Slvs.SLVS_FREE_IN_3D, 30.0, 101, 102, 0, 0) + + ' Let's tell the solver to keep the second point as close to constant + ' as possible, instead moving the first point. That's parameters + ' 4, 5, and 6. + slv.Solve(g, 4, 5, 6, 0, True) + + If (slv.GetResult() = Slvs.SLVS_RESULT_OKAY) Then + ' Note that we are referring to the parameters by their handles, + ' and not by their index in the list. This is a difference from + ' the C example. + Console.WriteLine(String.Format( _ + "okay; now at ({0:F3}, {1:F3}, {2:F3})", _ + slv.GetParamByHandle(1), slv.GetParamByHandle(2), _ + slv.GetParamByHandle(3))) + Console.WriteLine(String.Format( _ + " ({0:F3}, {1:F3}, {2:F3})", _ + slv.GetParamByHandle(4), slv.GetParamByHandle(5), _ + slv.GetParamByHandle(6))) + Console.WriteLine(slv.GetDof().ToString() + " DOF") + Else + Console.WriteLine("solve failed") + End If + End Sub + + + Sub Example2dWithHandles() + Dim g As UInteger + Dim qw, qx, qy, qz As Double + + Dim slv As New Slvs + + g = 1 + + ' First, we create our workplane. Its origin corresponds to the origin + ' of our base frame (x y z) = (0 0 0) + slv.AddParam(1, g, 0.0) + slv.AddParam(2, g, 0.0) + slv.AddParam(3, g, 0.0) + slv.AddPoint3d(101, g, 1, 2, 3) + ' and it is parallel to the xy plane, so it has basis vectors (1 0 0) + ' and (0 1 0). + slv.MakeQuaternion(1, 0, 0, _ + 0, 1, 0, qw, qx, qy, qz) + slv.AddParam(4, g, qw) + slv.AddParam(5, g, qx) + slv.AddParam(6, g, qy) + slv.AddParam(7, g, qz) + slv.AddNormal3d(102, g, 4, 5, 6, 7) + + slv.AddWorkplane(200, g, 101, 102) + + ' Now create a second group. We'll solve group 2, while leaving group 1 + ' constant; so the workplane that we've created will be locked down, + ' and the solver can't move it. + g = 2 + ' These points are represented by their coordinates (u v) within the + ' workplane, so they need only two parameters each. + slv.AddParam(11, g, 10.0) + slv.AddParam(12, g, 20.0) + slv.AddPoint2d(301, g, 200, 11, 12) + + slv.AddParam(13, g, 20.0) + slv.AddParam(14, g, 10.0) + slv.AddPoint2d(302, g, 200, 13, 14) + + ' And we create a line segment with those endpoints. + slv.AddLineSegment(400, g, 200, 301, 302) + + ' Now three more points. + slv.AddParam(15, g, 100.0) + slv.AddParam(16, g, 120.0) + slv.AddPoint2d(303, g, 200, 15, 16) + + slv.AddParam(17, g, 120.0) + slv.AddParam(18, g, 110.0) + slv.AddPoint2d(304, g, 200, 17, 18) + + slv.AddParam(19, g, 115.0) + slv.AddParam(20, g, 115.0) + slv.AddPoint2d(305, g, 200, 19, 20) + + ' And arc, centered at point 303, starting at point 304, ending at + ' point 305. + slv.AddArcOfCircle(401, g, 200, 102, 303, 304, 305) + + ' Now one more point, and a distance + slv.AddParam(21, g, 200.0) + slv.AddParam(22, g, 200.0) + slv.AddPoint2d(306, g, 200, 21, 22) + + slv.AddParam(23, g, 30.0) + slv.AddDistance(307, g, 200, 23) + + ' And a complete circle, centered at point 306 with radius equal to + ' distance 307. The normal is 102, the same as our workplane. + slv.AddCircle(402, g, 200, 306, 102, 307) + + ' The length of our line segment is 30.0 units. + slv.AddConstraint(1, g, Slvs.SLVS_C_PT_PT_DISTANCE, _ + 200, 30.0, 301, 302, 0, 0) + + ' And the distance from our line segment to the origin is 10.0 units. + slv.AddConstraint(2, g, Slvs.SLVS_C_PT_LINE_DISTANCE, _ + 200, 10.0, 101, 0, 400, 0) + + ' And the line segment is vertical. + slv.AddConstraint(3, g, Slvs.SLVS_C_VERTICAL, _ + 200, 0.0, 0, 0, 400, 0) + + ' And the distance from one endpoint to the origin is 15.0 units. + slv.AddConstraint(4, g, Slvs.SLVS_C_PT_PT_DISTANCE, _ + 200, 15.0, 301, 101, 0, 0) + + ' And same for the other endpoint; so if you add this constraint then + ' the sketch is overconstrained and will signal an error. + 'slv.AddConstraint(5, g, Slvs.SLVS_C_PT_PT_DISTANCE, _ + ' 200, 18.0, 302, 101, 0, 0) + + ' The arc and the circle have equal radius. + slv.AddConstraint(6, g, Slvs.SLVS_C_EQUAL_RADIUS, _ + 200, 0.0, 0, 0, 401, 402) + + ' The arc has radius 17.0 units. + slv.AddConstraint(7, g, Slvs.SLVS_C_DIAMETER, _ + 200, 2 * 17.0, 0, 0, 401, 0) + + ' If the solver fails, then ask it to report which constraints caused + ' the problem. + slv.Solve(g, 0, 0, 0, 0, True) + + If (slv.GetResult() = Slvs.SLVS_RESULT_OKAY) Then + Console.WriteLine("solved okay") + ' Note that we are referring to the parameters by their handles, + ' and not by their index in the list. This is a difference from + ' the C example. + Console.WriteLine(String.Format( _ + "line from ({0:F3} {1:F3}) to ({2:F3} {3:F3})", _ + slv.GetParamByHandle(11), slv.GetParamByHandle(12), _ + slv.GetParamByHandle(13), slv.GetParamByHandle(14))) + Console.WriteLine(String.Format( _ + "arc center ({0:F3} {1:F3}) start ({2:F3} {3:F3}) " + _ + "finish ({4:F3} {5:F3})", _ + slv.GetParamByHandle(15), slv.GetParamByHandle(16), _ + slv.GetParamByHandle(17), slv.GetParamByHandle(18), _ + slv.GetParamByHandle(19), slv.GetParamByHandle(20))) + Console.WriteLine(String.Format( _ + "circle center ({0:F3} {1:F3}) radius {2:F3}", _ + slv.GetParamByHandle(21), slv.GetParamByHandle(22), _ + slv.GetParamByHandle(23))) + + Console.WriteLine(slv.GetDof().ToString() + " DOF") + Else + Console.Write("solve failed; problematic constraints are:") + Dim t As UInteger + For Each t In slv.GetFaileds() + Console.Write(" " + t.ToString()) + Next + Console.WriteLine("") + If (slv.GetResult() = Slvs.SLVS_RESULT_INCONSISTENT) Then + Console.WriteLine("system inconsistent") + Else + Console.WriteLine("system nonconvergent") + End If + End If + + End Sub + + + ''''''''''''''''''''''''''''''' + ' The interface to the library, and the wrapper functions around + ' that interface, follow. + + ' These are the core functions imported from the DLL + _ + Public Sub Slvs_Solve(ByVal sys As IntPtr, ByVal hg As UInteger) + End Sub + + _ + Public Sub Slvs_MakeQuaternion( + ByVal ux As Double, ByVal uy As Double, ByVal uz As Double, + ByVal vx As Double, ByVal vy As Double, ByVal vz As Double, + ByRef qw As Double, ByRef qx As Double, ByRef qy As Double, + ByRef qz As Double) + End Sub + + ' And this is a thin wrapper around those functions, which provides + ' convenience functions similar to those provided in slvs.h for the C API. + Public Class Slvs + + Public Structure Slvs_Param + Public h As UInteger + Public group As UInteger + Public val As Double + End Structure + + Public Const SLVS_FREE_IN_3D As Integer = 0 + + Public Const SLVS_E_POINT_IN_3D As Integer = 50000 + Public Const SLVS_E_POINT_IN_2D As Integer = 50001 + Public Const SLVS_E_NORMAL_IN_3D As Integer = 60000 + Public Const SLVS_E_NORMAL_IN_2D As Integer = 60001 + Public Const SLVS_E_DISTANCE As Integer = 70000 + Public Const SLVS_E_WORKPLANE As Integer = 80000 + Public Const SLVS_E_LINE_SEGMENT As Integer = 80001 + Public Const SLVS_E_CUBIC As Integer = 80002 + Public Const SLVS_E_CIRCLE As Integer = 80003 + Public Const SLVS_E_ARC_OF_CIRCLE As Integer = 80004 + + Public Structure Slvs_Entity + Public h As UInteger + Public group As UInteger + + Public type As Integer + + Public wrkpl As UInteger + Public point0 As UInteger + Public point1 As UInteger + Public point2 As UInteger + Public point3 As UInteger + Public normal As UInteger + Public distance As UInteger + + Public param0 As UInteger + Public param1 As UInteger + Public param2 As UInteger + Public param3 As UInteger + End Structure + + Public Const SLVS_C_POINTS_COINCIDENT As Integer = 100000 + Public Const SLVS_C_PT_PT_DISTANCE As Integer = 100001 + Public Const SLVS_C_PT_PLANE_DISTANCE As Integer = 100002 + Public Const SLVS_C_PT_LINE_DISTANCE As Integer = 100003 + Public Const SLVS_C_PT_FACE_DISTANCE As Integer = 100004 + Public Const SLVS_C_PT_IN_PLANE As Integer = 100005 + Public Const SLVS_C_PT_ON_LINE As Integer = 100006 + Public Const SLVS_C_PT_ON_FACE As Integer = 100007 + Public Const SLVS_C_EQUAL_LENGTH_LINES As Integer = 100008 + Public Const SLVS_C_LENGTH_RATIO As Integer = 100009 + Public Const SLVS_C_EQ_LEN_PT_LINE_D As Integer = 100010 + Public Const SLVS_C_EQ_PT_LN_DISTANCES As Integer = 100011 + Public Const SLVS_C_EQUAL_ANGLE As Integer = 100012 + Public Const SLVS_C_EQUAL_LINE_ARC_LEN As Integer = 100013 + Public Const SLVS_C_SYMMETRIC As Integer = 100014 + Public Const SLVS_C_SYMMETRIC_HORIZ As Integer = 100015 + Public Const SLVS_C_SYMMETRIC_VERT As Integer = 100016 + Public Const SLVS_C_SYMMETRIC_LINE As Integer = 100017 + Public Const SLVS_C_AT_MIDPOINT As Integer = 100018 + Public Const SLVS_C_HORIZONTAL As Integer = 100019 + Public Const SLVS_C_VERTICAL As Integer = 100020 + Public Const SLVS_C_DIAMETER As Integer = 100021 + Public Const SLVS_C_PT_ON_CIRCLE As Integer = 100022 + Public Const SLVS_C_SAME_ORIENTATION As Integer = 100023 + Public Const SLVS_C_ANGLE As Integer = 100024 + Public Const SLVS_C_PARALLEL As Integer = 100025 + Public Const SLVS_C_PERPENDICULAR As Integer = 100026 + Public Const SLVS_C_ARC_LINE_TANGENT As Integer = 100027 + Public Const SLVS_C_CUBIC_LINE_TANGENT As Integer = 100028 + Public Const SLVS_C_EQUAL_RADIUS As Integer = 100029 + Public Const SLVS_C_PROJ_PT_DISTANCE As Integer = 100030 + Public Const SLVS_C_WHERE_DRAGGED As Integer = 100031 + Public Const SLVS_C_CURVE_CURVE_TANGENT As Integer = 100032 + Public Const SLVS_C_LENGTH_DIFFERENCE As Integer = 100033 + + Public Structure Slvs_Constraint + Public h As UInteger + Public group As UInteger + + Public type As Integer + + Public wrkpl As UInteger + + Public valA As Double + Public ptA As UInteger + Public ptB As UInteger + Public entityA As UInteger + Public entityB As UInteger + Public entityC As UInteger + Public entityD As UInteger + + Public other As Integer + Public other2 As Integer + End Structure + + Public Const SLVS_RESULT_OKAY As Integer = 0 + Public Const SLVS_RESULT_INCONSISTENT As Integer = 1 + Public Const SLVS_RESULT_DIDNT_CONVERGE As Integer = 2 + Public Const SLVS_RESULT_TOO_MANY_UNKNOWNS As Integer = 3 + + Public Structure Slvs_System + Public param As IntPtr + Public params As Integer + Public entity As IntPtr + Public entities As Integer + Public constraint As IntPtr + Public constraints As Integer + + Public dragged As IntPtr + Public ndragged As Integer + + Public calculatedFaileds As Integer + + Public failed As IntPtr + Public faileds As Integer + + Public dof As Integer + + Public result As Integer + End Structure + + Dim Params As New List(Of Slvs_Param) + Dim Entities As New List(Of Slvs_Entity) + Dim Constraints As New List(Of Slvs_Constraint) + + Dim Faileds As New List(Of UInteger) + + Dim Result As Integer + Dim Dof As Integer + + ' Return the value of a parameter, by its handle. This function + ' may be used, for example, to obtain the new values of the + ' parameters after a call to Solve(). + Public Function GetParamByHandle(ByVal h As UInteger) As Double + Dim t As Slvs_Param + For Each t In Params + If (t.h = h) Then + Return t.val + End If + Next + Throw New Exception("Invalid parameter handle.") + End Function + + ' Return the value of a parameter, by its index in the list (where + ' that index is determined by the order in which the parameters + ' were inserted with AddParam(), not by its handle). + Public Function GetParamByIndex(ByVal i As Integer) As Double + Return Params(i).val + End Function + + ' Get the result after a call to Solve(). This may be + ' SLVS_RESULT_OKAY - it worked + ' SLVS_RESULT_INCONSISTENT - failed, inconsistent + ' SLVS_RESULT_DIDNT_CONVERGE - consistent, but still failed + ' SLVS_RESULT_TOO_MANY_UNKNOWNS - too many parameters in one group + Public Function GetResult() As Integer + Return Result + End Function + + ' After a call to Solve(), this returns the number of unconstrained + ' degrees of freedom for the sketch. + Public Function GetDof() As Integer + Return Dof + End Function + + ' After a failing call to Solve(), this returns the list of + ' constraints, identified by their handle, that would fix the + ' system if they were deleted. This list will be populated only + ' if calculateFaileds is True in the Solve() call. + Public Function GetFaileds() As List(Of UInteger) + Return Faileds + End Function + + ' Clear our lists of entities, constraints, and parameters. + Public Sub ResetAll() + Params.Clear() + Entities.Clear() + Constraints.Clear() + End Sub + + + ''''''''''''''''''''''''''''''' + ' These functions are broadly similar to the Slvs_Make... + ' functions in slvs.h. See the file DOC.txt accompanying the + ' library for details. + + Public Sub AddParam(ByVal h As UInteger, ByVal group As UInteger, + ByVal val As Double) + Dim p As Slvs_Param + p.h = h + p.group = group + p.val = val + Params.Add(p) + End Sub + + Public Sub AddPoint2d(ByVal h As UInteger, ByVal group As UInteger, + ByVal wrkpl As UInteger, + ByVal u As UInteger, ByVal v As UInteger) + Dim e As Slvs_Entity + e.h = h + e.group = group + e.type = SLVS_E_POINT_IN_2D + e.wrkpl = wrkpl + e.param0 = u + e.param1 = v + Entities.Add(e) + End Sub + + Public Sub AddPoint3d(ByVal h As UInteger, ByVal group As UInteger, + ByVal x As UInteger, ByVal y As UInteger, ByVal z As UInteger) + Dim e As Slvs_Entity + e.h = h + e.group = group + e.type = SLVS_E_POINT_IN_3D + e.wrkpl = SLVS_FREE_IN_3D + e.param0 = x + e.param1 = y + e.param2 = z + Entities.Add(e) + End Sub + + Public Sub AddNormal3d(ByVal h As UInteger, ByVal group As UInteger, + ByVal qw As UInteger, ByVal qx As UInteger, + ByVal qy As UInteger, ByVal qz As UInteger) + Dim e As Slvs_Entity + e.h = h + e.group = group + e.type = SLVS_E_NORMAL_IN_3D + e.wrkpl = SLVS_FREE_IN_3D + e.param0 = qw + e.param1 = qx + e.param2 = qy + e.param3 = qz + Entities.Add(e) + End Sub + + Public Sub AddNormal2d(ByVal h As UInteger, ByVal group As UInteger, + ByVal wrkpl As UInteger) + Dim e As Slvs_Entity + e.h = h + e.group = group + e.type = SLVS_E_NORMAL_IN_2D + e.wrkpl = wrkpl + Entities.Add(e) + End Sub + + Public Sub AddDistance(ByVal h As UInteger, ByVal group As UInteger, + ByVal wrkpl As UInteger, ByVal d As UInteger) + Dim e As Slvs_Entity + e.h = h + e.group = group + e.type = SLVS_E_DISTANCE + e.wrkpl = wrkpl + e.param0 = d + Entities.Add(e) + End Sub + + Public Sub AddLineSegment(ByVal h As UInteger, ByVal group As UInteger, + ByVal wrkpl As UInteger, + ByVal ptA As UInteger, ByVal ptB As UInteger) + Dim e As Slvs_Entity + e.h = h + e.group = group + e.type = SLVS_E_LINE_SEGMENT + e.wrkpl = wrkpl + e.point0 = ptA + e.point1 = ptB + Entities.Add(e) + End Sub + + Public Sub AddCubic(ByVal h As UInteger, ByVal group As UInteger, + ByVal wrkpl As UInteger, + ByVal pt0 As UInteger, ByVal pt1 As UInteger, + ByVal pt2 As UInteger, ByVal pt3 As UInteger) + Dim e As Slvs_Entity + e.h = h + e.group = group + e.type = SLVS_E_CUBIC + e.wrkpl = wrkpl + e.point0 = pt0 + e.point1 = pt1 + e.point2 = pt2 + e.point3 = pt3 + Entities.Add(e) + End Sub + + Public Sub AddArcOfCircle(ByVal h As UInteger, ByVal group As UInteger, + ByVal wrkpl As UInteger, + ByVal normal As UInteger, + ByVal center As UInteger, + ByVal pstart As UInteger, + ByVal pend As UInteger) + Dim e As Slvs_Entity + e.h = h + e.group = group + e.type = SLVS_E_ARC_OF_CIRCLE + e.wrkpl = wrkpl + e.normal = normal + e.point0 = center + e.point1 = pstart + e.point2 = pend + Entities.Add(e) + End Sub + + Public Sub AddCircle(ByVal h As UInteger, ByVal group As UInteger, + ByVal wrkpl As UInteger, + ByVal center As UInteger, ByVal normal As UInteger, + ByVal radius As UInteger) + Dim e As Slvs_Entity + e.h = h + e.group = group + e.type = SLVS_E_CIRCLE + e.wrkpl = wrkpl + e.point0 = center + e.normal = normal + e.distance = radius + Entities.Add(e) + End Sub + + Public Sub AddWorkplane(ByVal h As UInteger, ByVal group As UInteger, + ByVal origin As UInteger, + ByVal normal As UInteger) + Dim e As Slvs_Entity + e.h = h + e.group = group + e.type = SLVS_E_WORKPLANE + e.wrkpl = SLVS_FREE_IN_3D + e.point0 = origin + e.normal = normal + Entities.Add(e) + End Sub + + Public Sub AddConstraint(ByVal h As UInteger, + ByVal group As UInteger, + ByVal type As Integer, + ByVal wrkpl As UInteger, + ByVal valA As Double, + ByVal ptA As UInteger, + ByVal ptB As UInteger, + ByVal entityA As UInteger, + ByVal entityB As UInteger) + Dim c As Slvs_Constraint + c.h = h + c.group = group + c.type = type + c.wrkpl = wrkpl + c.valA = valA + c.ptA = ptA + c.ptB = ptB + c.entityA = entityA + c.entityB = entityB + Constraints.Add(c) + End Sub + + ' Solve the system. The geometry of the system must already have + ' been specified through the Add...() calls. The result of the + ' solution process may be obtained by calling GetResult(), + ' GetFaileds(), GetDof(), and GetParamByXXX(). + ' + ' The parameters dragged (indicated by their handles) will be held + ' as close as possible to their original positions, even if this + ' results in large moves for other parameters. This feature may be + ' useful if, for example, the user is dragging the point whose + ' location is defined by those parameters. + Public Sub Solve(ByVal group As UInteger, + ByVal dragged As List(Of UInteger), + ByVal calculateFaileds As Boolean) + Dim i As Integer + + Dim pp, p(Params.Count()) As Slvs_Param + i = 0 + For Each pp In Params + p(i) = pp + i += 1 + Next + + Dim ee, e(Entities.Count()) As Slvs_Entity + i = 0 + For Each ee In Entities + e(i) = ee + i += 1 + Next + + Dim cc, c(Constraints.Count()) As Slvs_Constraint + i = 0 + For Each cc In Constraints + c(i) = cc + i += 1 + Next + Dim f(Constraints.Count()) As UInteger + + Dim dd, d(dragged.Count()) As UInteger + i = 0 + For Each cc In dragged + d(i) = dd + i += 1 + Next + + Dim sys As Slvs_System + + Dim pgc, egc, cgc, dgc As GCHandle + pgc = GCHandle.Alloc(p, GCHandleType.Pinned) + sys.param = pgc.AddrOfPinnedObject() + sys.params = Params.Count() + egc = GCHandle.Alloc(e, GCHandleType.Pinned) + sys.entity = egc.AddrOfPinnedObject() + sys.entities = Entities.Count() + cgc = GCHandle.Alloc(c, GCHandleType.Pinned) + sys.constraint = cgc.AddrOfPinnedObject() + sys.constraints = Constraints.Count() + dgc = GCHandle.Alloc(d, GCHandleType.Pinned) + sys.dragged = dgc.AddrOfPinnedObject() + sys.ndragged = dragged.Count() + + Dim fgc As GCHandle + fgc = GCHandle.Alloc(f, GCHandleType.Pinned) + If calculateFaileds Then + sys.calculatedFaileds = 1 + Else + sys.calculatedFaileds = 0 + End If + sys.faileds = Constraints.Count() + sys.failed = fgc.AddrOfPinnedObject() + + Dim sysgc As GCHandle + sysgc = GCHandle.Alloc(sys, GCHandleType.Pinned) + + Slvs_Solve(sysgc.AddrOfPinnedObject(), group) + + sys = sysgc.Target + + For i = 0 To Params.Count() - 1 + Params(i) = p(i) + Next + + Faileds.Clear() + For i = 0 To sys.faileds - 1 + Faileds.Add(f(i)) + Next + + sysgc.Free() + fgc.Free() + pgc.Free() + egc.Free() + cgc.Free() + dgc.Free() + + Result = sys.result + Dof = sys.dof + End Sub + ' A simpler version of the function, if the parameters being dragged + ' correspond to a single point. + Public Sub Solve(ByVal group As UInteger, ByVal dragged As Point, + ByVal calculatedFaileds As Boolean) + If TypeOf dragged Is Point2d Then + Dim p As Point2d + p = dragged + Dim d As New List(Of UInteger)({ p.up.H, p.vp.H }) + Solve(group, d, calculatedFaileds) + ElseIf TypeOf dragged Is Point3d Then + Dim p As Point3d + p = dragged + Dim d As New List(Of UInteger)({ p.xp.H, p.yp.H, p.zp.H }) + Solve(group, d, calculatedFaileds) + Else + Throw New Exception("Can't get dragged params for point.") + End If + End Sub + ' or if it's a single distance (e.g., the radius of a circle) + Public Sub Solve(ByVal group As UInteger, ByVal dragged As Distance, + ByVal calculatedFaileds As Boolean) + Dim d As New List(Of UInteger)({ dragged.dp.H }) + Solve(group, d, calculatedFaileds) + End Sub + ' or if it's nothing. + Public Sub Solve(ByVal group As UInteger, + ByVal calculateFaileds As Boolean) + Solve(group, 0, 0, 0, 0, calculateFaileds) + End Sub + + ' Return the quaternion in (qw, qx, qy, qz) that represents a + ' rotation from the base frame to a coordinate system with the + ' specified basis vectors u and v. For example, u = (0, 1, 0) + ' and v = (0, 0, 1) specifies the yz plane, such that a point with + ' (u, v) = (7, 12) has (x, y, z) = (0, 7, 12). + Public Sub MakeQuaternion( + ByVal ux As Double, ByVal uy As Double, ByVal uz As Double, + ByVal vx As Double, ByVal vy As Double, ByVal vz As Double, + ByRef qw As Double, ByRef qx As Double, ByRef qy As Double, + ByRef qz As Double) + Slvs_MakeQuaternion(ux, uy, uz, _ + vx, vy, vz, _ + qw, qx, qy, qz) + End Sub + + Public Function FreeIn3d() + Return New Workplane(Me) + End Function + + + ''''''''''''''''''''''''''''''' + ' Functions to create the object-oriented wrappers defined below. + + Public Function NewParam(ByVal group As UInteger, ByVal val As Double) + Return New Param(Me, group, val) + End Function + Public Function NewPoint2d(ByVal group As UInteger, + ByVal wrkpl As Workplane, + ByVal u As Double, ByVal v As Double) + Return New Point2d(Me, group, wrkpl, u, v) + End Function + Public Function NewPoint2d(ByVal group As UInteger, + ByVal wrkpl As Workplane, + ByVal u As Param, ByVal v As Param) + Return New Point2d(Me, group, wrkpl, u, v) + End Function + Public Function NewPoint3d(ByVal group As UInteger, + ByVal x As Double, + ByVal y As Double, + ByVal z As Double) + Return New Point3d(Me, group, x, y, z) + End Function + Public Function NewPoint3d(ByVal group As UInteger, + ByVal x As Param, + ByVal y As Param, + ByVal z As Param) + Return New Point3d(Me, group, x, y, z) + End Function + Public Function NewNormal3d(ByVal group As UInteger, + ByVal ux As Double, ByVal uy As Double, ByVal uz As Double, + ByVal vx As Double, ByVal vy As Double, ByVal vz As Double) + Return New Normal3d(Me, group, ux, uy, uz, vx, vy, vz) + End Function + Public Function NewNormal3d(ByVal group As UInteger, + ByVal qw As Param, ByVal qx As Param, + ByVal qy As Param, ByVal qz As Param) + Return New Normal3d(Me, group, qw, qx, qy, qz) + End Function + Public Function NewNormal2d(ByVal group As UInteger, + ByVal wrkpl As Workplane) + Return New Normal2d(Me, group, wrkpl) + End Function + Public Function NewDistance(ByVal group As UInteger, + ByVal wrkpl As Workplane, ByVal d As Double) + Return New Distance(Me, group, wrkpl, d) + End Function + Public Function NewDistance(ByVal group As UInteger, + ByVal wrkpl As Workplane, ByVal d As Param) + Return New Distance(Me, group, wrkpl, d) + End Function + Public Function NewLineSegment(ByVal group As UInteger, + ByVal wrkpl As Workplane, + ByVal ptA As Point, ByVal ptB As Point) + Return New LineSegment(Me, group, wrkpl, ptA, ptB) + End Function + Public Function NewArcOfCircle(ByVal group As UInteger, + ByVal wrkpl As Workplane, + ByVal normal As Normal, + ByVal center As Point, + ByVal pstart As Point, + ByVal pend As Point) + Return New ArcOfCircle(Me, group, wrkpl, normal, _ + center, pstart, pend) + End Function + Public Function NewCircle(ByVal group As UInteger, + ByVal wrkpl As Workplane, + ByVal center As Point, + ByVal normal As Normal, + ByVal radius As Distance) + Return New Circle(Me, group, wrkpl, center, normal, radius) + End Function + Public Function NewWorkplane(ByVal group As UInteger, + ByVal origin As Point, + ByVal normal As Normal) + Return New Workplane(Me, group, origin, normal) + End Function + + Public Sub AddConstraint(ByVal H As UInteger, ByVal group As UInteger, + ByVal type As Integer, + ByVal wrkpl As Workplane, + ByVal valA As Double, + ByVal ptA As Point, ByVal ptB As Point, + ByVal entityA As Entity, + ByVal entityB As Entity) + AddConstraint(H, group, type, _ + If(IsNothing(wrkpl), 0, wrkpl.H), _ + valA, _ + If(IsNothing(ptA), 0, ptA.H), _ + If(IsNothing(ptB), 0, ptB.H), _ + If(IsNothing(entityA), 0, entityA.H), _ + If(IsNothing(entityB), 0, entityB.H)) + End Sub + + + ''''''''''''''''''''''''''''''' + ' The object-oriented wrapper classes themselves, to allow the + ' representation of entities and constraints as .net objects, not + ' integer handles. These don't do any work themselves, beyond + ' allocating and storing a unique integer handle. + ' + ' These functions will assign parameters and entities with + ' consecutive handles starting from 1. If they are intermixed + ' with parameters and entities with user-specified handles, then + ' those handles must be chosen not to conflict, e.g. starting + ' from 100 000 or another large number. + + Public Class Param + Public Slv As Slvs + Public H As UInteger + + Public Sub New(ByVal s As Slvs, ByVal group As UInteger, + ByVal val As Double) + Slv = s + H = Slv.Params.Count() + 1 + Slv.AddParam(H, group, val) + End Sub + End Class + + Public MustInherit Class Entity + Public Slv As Slvs + Public H As UInteger + + Public Sub New(ByVal s As Slvs) + Slv = s + H = Slv.Entities.Count() + 1 + End Sub + End Class + + Public MustInherit Class Point + Inherits Entity + Public Sub New(ByVal s As Slvs) + MyBase.New(s) + End Sub + End Class + + Public MustInherit Class Normal + Inherits Entity + Public Sub New(ByVal s As Slvs) + MyBase.New(s) + End Sub + End Class + + Public Class Point2d + Inherits Point + Public up, vp As Param + Public Sub New(ByVal s As Slvs, ByVal group As UInteger, + ByVal wrkpl As Workplane, + ByVal u As Double, ByVal v As Double) + MyBase.New(s) + up = New Param(Slv, group, u) + vp = New Param(Slv, group, v) + Slv.AddPoint2d(H, group, wrkpl.H, up.H, vp.H) + End Sub + Public Sub New(ByVal s As Slvs, ByVal group As UInteger, + ByVal wrkpl As Workplane, + ByVal u As Param, ByVal v As Param) + MyBase.New(s) + Slv.AddPoint2d(H, group, wrkpl.H, u.H, v.H) + up = u + vp = v + End Sub + Function GetU() + Return Slv.GetParamByHandle(up.H) + End Function + Function GetV() + Return Slv.GetParamByHandle(vp.H) + End Function + End Class + + Public Class Point3d + Inherits Point + Public xp, yp, zp As Param + Public Sub New(ByVal s As Slvs, ByVal group As UInteger, + ByVal x As Double, ByVal y As Double, + ByVal z As Double) + MyBase.New(s) + xp = New Param(Slv, group, x) + yp = New Param(Slv, group, y) + zp = New Param(Slv, group, z) + Slv.AddPoint3d(H, group, xp.H, yp.H, zp.H) + End Sub + Public Sub New(ByVal s As Slvs, ByVal group As UInteger, + ByVal x As Param, ByVal y As Param, ByVal z As Param) + MyBase.New(s) + Slv.AddPoint3d(H, group, x.H, y.H, z.H) + xp = x + yp = y + zp = z + End Sub + Function GetX() + Return Slv.GetParamByHandle(xp.H) + End Function + Function GetY() + Return Slv.GetParamByHandle(yp.H) + End Function + Function GetZ() + Return Slv.GetParamByHandle(zp.H) + End Function + End Class + + Public Class Normal3d + Inherits Normal + Dim qwp, qxp, qyp, qzp As Param + Public Sub New(ByVal s As Slvs, ByVal group As UInteger, + ByVal ux As Double, ByVal uy As Double, ByVal uz As Double, + ByVal vx As Double, ByVal vy As Double, ByVal vz As Double) + MyBase.New(s) + Dim qw, qx, qy, qz As Double + Slv.MakeQuaternion(ux, uy, uz, vx, vy, vz, qw, qx, qy, qz) + qwp = New Param(Slv, group, qw) + qxp = New Param(Slv, group, qx) + qyp = New Param(Slv, group, qy) + qzp = New Param(Slv, group, qz) + Slv.AddNormal3d(H, group, qwp.H, qxp.H, qyp.H, qzp.H) + End Sub + Public Sub New(ByVal s As Slvs, ByVal group As UInteger, + ByVal qw As Param, ByVal qx As Param, + ByVal qy As Param, ByVal qz As Param) + MyBase.New(s) + Slv.AddNormal3d(H, group, qw.H, qx.H, qy.H, qz.H) + qwp = qw + qxp = qx + qyp = qy + qzp = qz + End Sub + End Class + + Public Class Normal2d + Inherits Normal + Public Sub New(ByVal s As Slvs, ByVal group As UInteger, + ByVal wrkpl As Workplane) + MyBase.New(s) + Slv.AddNormal2d(H, group, wrkpl.H) + End Sub + End Class + + Public Class Distance + Inherits Entity + Public dp As Param + + Public Sub New(ByVal s As Slvs, ByVal group As UInteger, + ByVal wrkpl As Workplane, ByVal d As Double) + MyBase.New(s) + dp = New Param(Slv, group, d) + Slv.AddDistance(H, group, wrkpl.H, dp.H) + End Sub + Public Sub New(ByVal s As Slvs, ByVal group As UInteger, + ByVal wrkpl As Workplane, ByVal d As Param) + MyBase.New(s) + Slv.AddDistance(H, group, wrkpl.H, d.H) + dp = d + End Sub + Function GetDistance() As Double + Return Slv.GetParamByHandle(dp.H) + End Function + End Class + + Public Class LineSegment + Inherits Entity + Public Sub New(ByVal s As Slvs, ByVal group As UInteger, + ByVal wrkpl As Workplane, + ByVal ptA As Point, ByVal ptB As Point) + MyBase.New(s) + Slv.AddLineSegment(H, group, wrkpl.H, ptA.H, ptB.H) + End Sub + End Class + + Public Class Cubic + Inherits Entity + Public Sub New(ByVal s As Slvs, ByVal group As UInteger, + ByVal wrkpl As Workplane, + ByVal pt0 As Point, ByVal pt1 As Point, + ByVal pt2 As Point, ByVal pt3 As Point) + MyBase.New(s) + Slv.AddCubic(H, group, wrkpl.H, pt0.H, pt1.H, pt2.H, pt3.H) + End Sub + End Class + + Public Class ArcOfCircle + Inherits Entity + Public Sub New(ByVal s As Slvs, ByVal group As UInteger, + ByVal wrkpl As Workplane, ByVal normal As Normal, + ByVal center As Point, ByVal pstart As Point, + ByVal pend As Point) + MyBase.New(s) + Slv.AddArcOfCircle(H, group, wrkpl.H, normal.H, _ + center.H, pstart.H, pend.H) + End Sub + End Class + + Public Class Circle + Inherits Entity + Public Sub New(ByVal s As Slvs, ByVal group As UInteger, + ByVal wrkpl As Workplane, ByVal center As Point, + ByVal normal As Normal, ByVal radius As Distance) + MyBase.New(s) + Slv.AddCircle(H, group, wrkpl.H, center.H, normal.H, radius.H) + End Sub + End Class + + Public Class Workplane + Inherits Entity + Public Sub New(ByVal s As Slvs) + MyBase.New(s) + H = SLVS_FREE_IN_3D + End Sub + Public Sub New(ByVal s As Slvs, ByVal group As UInteger, + ByVal origin As Point, ByVal normal As Normal) + MyBase.New(s) + Slv.AddWorkplane(H, group, origin.H, normal.H) + End Sub + End Class + + End Class + +End Module + diff --git a/include/slvs.h b/include/slvs.h new file mode 100644 index 0000000..0e58d46 --- /dev/null +++ b/include/slvs.h @@ -0,0 +1,516 @@ +/*----------------------------------------------------------------------------- + * Data structures and prototypes for slvs.lib, a geometric constraint solver. + * + * See the comments in this file, the accompanying sample code that uses + * this library, and the accompanying documentation (DOC.txt). + * + * Copyright 2009-2013 Jonathan Westhues. + *---------------------------------------------------------------------------*/ + +#ifndef __SLVS_H +#define __SLVS_H + +#if defined(WIN32) && !defined(STATIC_LIB) +# ifdef EXPORT_DLL +# define DLL __declspec( dllexport ) +# else +# define DLL __declspec( dllimport ) +# endif +#else +# define DLL +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef _MSC_VER +typedef unsigned __int32 uint32_t; +#else +#include +#endif +#include +#include + +typedef uint32_t Slvs_hParam; +typedef uint32_t Slvs_hEntity; +typedef uint32_t Slvs_hConstraint; +typedef uint32_t Slvs_hGroup; + +/* To obtain the 3d (not projected into a workplane) of a constraint or + * an entity, specify this instead of the workplane. */ +#define SLVS_FREE_IN_3D 0 + +typedef struct { + Slvs_hParam h; + Slvs_hGroup group; + double val; +} Slvs_Param; + + +#define SLVS_E_POINT_IN_3D 50000 +#define SLVS_E_POINT_IN_2D 50001 + +#define SLVS_E_NORMAL_IN_3D 60000 +#define SLVS_E_NORMAL_IN_2D 60001 + +#define SLVS_E_DISTANCE 70000 + +/* The special point, normal, and distance types used for parametric step + * and repeat, extrude, and assembly are currently not exposed. Please + * contact us if you are interested in using these. */ + +#define SLVS_E_WORKPLANE 80000 +#define SLVS_E_LINE_SEGMENT 80001 +#define SLVS_E_CUBIC 80002 +#define SLVS_E_CIRCLE 80003 +#define SLVS_E_ARC_OF_CIRCLE 80004 + +typedef struct { + Slvs_hEntity h; + Slvs_hGroup group; + + int type; + + Slvs_hEntity wrkpl; + Slvs_hEntity point[4]; + Slvs_hEntity normal; + Slvs_hEntity distance; + + Slvs_hParam param[4]; +} Slvs_Entity; + +#define SLVS_C_POINTS_COINCIDENT 100000 +#define SLVS_C_PT_PT_DISTANCE 100001 +#define SLVS_C_PT_PLANE_DISTANCE 100002 +#define SLVS_C_PT_LINE_DISTANCE 100003 +#define SLVS_C_PT_FACE_DISTANCE 100004 +#define SLVS_C_PT_IN_PLANE 100005 +#define SLVS_C_PT_ON_LINE 100006 +#define SLVS_C_PT_ON_FACE 100007 +#define SLVS_C_EQUAL_LENGTH_LINES 100008 +#define SLVS_C_LENGTH_RATIO 100009 +#define SLVS_C_EQ_LEN_PT_LINE_D 100010 +#define SLVS_C_EQ_PT_LN_DISTANCES 100011 +#define SLVS_C_EQUAL_ANGLE 100012 +#define SLVS_C_EQUAL_LINE_ARC_LEN 100013 +#define SLVS_C_SYMMETRIC 100014 +#define SLVS_C_SYMMETRIC_HORIZ 100015 +#define SLVS_C_SYMMETRIC_VERT 100016 +#define SLVS_C_SYMMETRIC_LINE 100017 +#define SLVS_C_AT_MIDPOINT 100018 +#define SLVS_C_HORIZONTAL 100019 +#define SLVS_C_VERTICAL 100020 +#define SLVS_C_DIAMETER 100021 +#define SLVS_C_PT_ON_CIRCLE 100022 +#define SLVS_C_SAME_ORIENTATION 100023 +#define SLVS_C_ANGLE 100024 +#define SLVS_C_PARALLEL 100025 +#define SLVS_C_PERPENDICULAR 100026 +#define SLVS_C_ARC_LINE_TANGENT 100027 +#define SLVS_C_CUBIC_LINE_TANGENT 100028 +#define SLVS_C_EQUAL_RADIUS 100029 +#define SLVS_C_PROJ_PT_DISTANCE 100030 +#define SLVS_C_WHERE_DRAGGED 100031 +#define SLVS_C_CURVE_CURVE_TANGENT 100032 +#define SLVS_C_LENGTH_DIFFERENCE 100033 +#define SLVS_C_ARC_ARC_LEN_RATIO 100034 +#define SLVS_C_ARC_LINE_LEN_RATIO 100035 +#define SLVS_C_ARC_ARC_DIFFERENCE 100036 +#define SLVS_C_ARC_LINE_DIFFERENCE 100037 + +typedef struct { + Slvs_hConstraint h; + Slvs_hGroup group; + + int type; + + Slvs_hEntity wrkpl; + + double valA; + Slvs_hEntity ptA; + Slvs_hEntity ptB; + Slvs_hEntity entityA; + Slvs_hEntity entityB; + Slvs_hEntity entityC; + Slvs_hEntity entityD; + + int other; + int other2; +} Slvs_Constraint; + + +typedef struct { + /*** INPUT VARIABLES + * + * Here, we specify the parameters and their initial values, the entities, + * and the constraints. For example, param[] points to the array of + * parameters, which has length params, so that the last valid element + * is param[params-1]. + * + * param[] is actually an in/out variable; if the solver is successful, + * then the new values (that satisfy the constraints) are written to it. */ + Slvs_Param *param; + int params; + Slvs_Entity *entity; + int entities; + Slvs_Constraint *constraint; + int constraints; + + /* If a parameter corresponds to a point (distance, normal, etc.) being + * dragged, then specify it here. This will cause the solver to favor + * that parameter, and attempt to change it as little as possible even + * if that requires it to change other parameters more. + * + * Note that the solver is still allowed to change the values of parameters + * specified this way. If you want to lock a dragged entity in place, use + * a SLVS_C_WHERE_DRAGGED constraint (though note that it can overconstrain + * e.g in case the dragged entity has less than two degrees of freedom in + * a workplane or less than 3 degrees of freedom in free space). */ + Slvs_hParam *dragged; + int ndragged; + + + /* If the solver fails, then it can determine which constraints are + * causing the problem. But this is a relatively slow process (for + * a system with n constraints, about n times as long as just solving). + * If calculateFaileds is true, then the solver will do so, otherwise + * not. */ + int calculateFaileds; + + /*** OUTPUT VARIABLES + * + * If the solver fails, then it can report which constraints are causing + * the problem. The caller should allocate the array failed[], and pass + * its size in faileds. + * + * The solver will set faileds equal to the number of problematic + * constraints, and write their Slvs_hConstraints into failed[]. To + * ensure that there is sufficient space for any possible set of + * failing constraints, faileds should be greater than or equal to + * constraints. */ + Slvs_hConstraint *failed; + int faileds; + + /* The solver indicates the number of unconstrained degrees of freedom. */ + int dof; + + /* The solver indicates whether the solution succeeded. */ +#define SLVS_RESULT_OKAY 0 +#define SLVS_RESULT_INCONSISTENT 1 +#define SLVS_RESULT_DIDNT_CONVERGE 2 +#define SLVS_RESULT_TOO_MANY_UNKNOWNS 3 +#define SLVS_RESULT_REDUNDANT_OKAY 4 + int result; +} Slvs_System; + +typedef struct { + int result; + int dof; + int nbad; +} Slvs_SolveResult; + +/* Our base coordinate system has basis vectors + * (1, 0, 0) (0, 1, 0) (0, 0, 1) + * A unit quaternion defines a rotation to a new coordinate system with + * basis vectors + * U V N + * which these functions compute from the quaternion. */ +DLL void Slvs_QuaternionU(double qw, double qx, double qy, double qz, + double *x, double *y, double *z); +DLL void Slvs_QuaternionV(double qw, double qx, double qy, double qz, + double *x, double *y, double *z); +DLL void Slvs_QuaternionN(double qw, double qx, double qy, double qz, + double *x, double *y, double *z); + +/* Similarly, compute a unit quaternion in terms of two basis vectors. */ +DLL void Slvs_MakeQuaternion(double ux, double uy, double uz, + double vx, double vy, double vz, + double *qw, double *qx, double *qy, double *qz); + + +/*------------------------------------- + * These are just convenience functions, to save you the trouble of filling + * out the structures by hand. The code is included in the header file to + * let the compiler inline them if possible. */ + +static inline Slvs_Param Slvs_MakeParam(Slvs_hParam h, Slvs_hGroup group, double val) +{ + Slvs_Param r; + r.h = h; + r.group = group; + r.val = val; + return r; +} +static inline Slvs_Entity Slvs_MakePoint2d(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl, + Slvs_hParam u, Slvs_hParam v) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_POINT_IN_2D; + r.wrkpl = wrkpl; + r.param[0] = u; + r.param[1] = v; + return r; +} +static inline Slvs_Entity Slvs_MakePoint3d(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hParam x, Slvs_hParam y, Slvs_hParam z) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_POINT_IN_3D; + r.wrkpl = SLVS_FREE_IN_3D; + r.param[0] = x; + r.param[1] = y; + r.param[2] = z; + return r; +} +static inline Slvs_Entity Slvs_MakeNormal3d(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hParam qw, Slvs_hParam qx, + Slvs_hParam qy, Slvs_hParam qz) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_NORMAL_IN_3D; + r.wrkpl = SLVS_FREE_IN_3D; + r.param[0] = qw; + r.param[1] = qx; + r.param[2] = qy; + r.param[3] = qz; + return r; +} +static inline Slvs_Entity Slvs_MakeNormal2d(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_NORMAL_IN_2D; + r.wrkpl = wrkpl; + return r; +} +static inline Slvs_Entity Slvs_MakeDistance(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl, Slvs_hParam d) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_DISTANCE; + r.wrkpl = wrkpl; + r.param[0] = d; + return r; +} +static inline Slvs_Entity Slvs_MakeLineSegment(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl, + Slvs_hEntity ptA, Slvs_hEntity ptB) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_LINE_SEGMENT; + r.wrkpl = wrkpl; + r.point[0] = ptA; + r.point[1] = ptB; + return r; +} +static inline Slvs_Entity Slvs_MakeCubic(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl, + Slvs_hEntity pt0, Slvs_hEntity pt1, + Slvs_hEntity pt2, Slvs_hEntity pt3) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_CUBIC; + r.wrkpl = wrkpl; + r.point[0] = pt0; + r.point[1] = pt1; + r.point[2] = pt2; + r.point[3] = pt3; + return r; +} +static inline Slvs_Entity Slvs_MakeArcOfCircle(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl, + Slvs_hEntity normal, + Slvs_hEntity center, + Slvs_hEntity start, Slvs_hEntity end) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_ARC_OF_CIRCLE; + r.wrkpl = wrkpl; + r.normal = normal; + r.point[0] = center; + r.point[1] = start; + r.point[2] = end; + return r; +} +static inline Slvs_Entity Slvs_MakeCircle(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity wrkpl, + Slvs_hEntity center, + Slvs_hEntity normal, Slvs_hEntity radius) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_CIRCLE; + r.wrkpl = wrkpl; + r.point[0] = center; + r.normal = normal; + r.distance = radius; + return r; +} +static inline Slvs_Entity Slvs_MakeWorkplane(Slvs_hEntity h, Slvs_hGroup group, + Slvs_hEntity origin, Slvs_hEntity normal) +{ + Slvs_Entity r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = SLVS_E_WORKPLANE; + r.wrkpl = SLVS_FREE_IN_3D; + r.point[0] = origin; + r.normal = normal; + return r; +} + +static inline Slvs_Constraint Slvs_MakeConstraint(Slvs_hConstraint h, + Slvs_hGroup group, + int type, + Slvs_hEntity wrkpl, + double valA, + Slvs_hEntity ptA, + Slvs_hEntity ptB, + Slvs_hEntity entityA, + Slvs_hEntity entityB) +{ + Slvs_Constraint r; + memset(&r, 0, sizeof(r)); + r.h = h; + r.group = group; + r.type = type; + r.wrkpl = wrkpl; + r.valA = valA; + r.ptA = ptA; + r.ptB = ptB; + r.entityA = entityA; + r.entityB = entityB; + return r; +} + +DLL bool Slvs_IsFreeIn3D(Slvs_Entity e); +DLL bool Slvs_Is3D(Slvs_Entity e); +DLL bool Slvs_IsNone(Slvs_Entity e); +DLL bool Slvs_IsPoint2D(Slvs_Entity e); +DLL bool Slvs_IsPoint3D(Slvs_Entity e); +DLL bool Slvs_IsNormal2D(Slvs_Entity e); +DLL bool Slvs_IsNormal3D(Slvs_Entity e); +DLL bool Slvs_IsLine(Slvs_Entity e); +DLL bool Slvs_IsLine2D(Slvs_Entity e); +DLL bool Slvs_IsLine3D(Slvs_Entity e); +DLL bool Slvs_IsCubic(Slvs_Entity e); +DLL bool Slvs_IsArc(Slvs_Entity e); +DLL bool Slvs_IsWorkplane(Slvs_Entity e); +DLL bool Slvs_IsDistance(Slvs_Entity e); +DLL bool Slvs_IsPoint(Slvs_Entity e); +DLL bool Slvs_IsCircle(Slvs_Entity e); + +static const Slvs_Entity SLVS_E_NONE = { 0 }; +static const Slvs_Entity SLVS_E_FREE_IN_3D = { 0 }; + +DLL Slvs_Entity Slvs_AddPoint2D(uint32_t grouph, double u, double v, Slvs_Entity workplane); +DLL Slvs_Entity Slvs_AddPoint3D(uint32_t grouph, double x, double y, double z); +DLL Slvs_Entity Slvs_AddNormal2D(uint32_t grouph, Slvs_Entity workplane); +DLL Slvs_Entity Slvs_AddNormal3D(uint32_t grouph, double qw, double qx, double qy, double qz); +DLL Slvs_Entity Slvs_AddDistance(uint32_t grouph, double value, Slvs_Entity workplane); +DLL Slvs_Entity Slvs_AddLine2D(uint32_t grouph, Slvs_Entity ptA, Slvs_Entity ptB, Slvs_Entity workplane); +DLL Slvs_Entity Slvs_AddLine3D(uint32_t grouph, Slvs_Entity ptA, Slvs_Entity ptB); +DLL Slvs_Entity Slvs_AddCubic(uint32_t grouph, Slvs_Entity ptA, Slvs_Entity ptB, Slvs_Entity ptC, Slvs_Entity ptD, Slvs_Entity workplane); +DLL Slvs_Entity Slvs_AddArc(uint32_t grouph, Slvs_Entity normal, Slvs_Entity center, Slvs_Entity start, Slvs_Entity end, Slvs_Entity workplane); +DLL Slvs_Entity Slvs_AddCircle(uint32_t grouph, Slvs_Entity normal, Slvs_Entity center, Slvs_Entity radius, Slvs_Entity workplane); +DLL Slvs_Entity Slvs_AddWorkplane(uint32_t grouph, Slvs_Entity origin, Slvs_Entity nm); +DLL Slvs_Entity Slvs_AddBase2D(uint32_t grouph); + + +DLL Slvs_Constraint Slvs_AddConstraint(uint32_t grouph, int type, Slvs_Entity workplane, double val, Slvs_Entity ptA, + Slvs_Entity ptB, Slvs_Entity entityA, + Slvs_Entity entityB, Slvs_Entity entityC, + Slvs_Entity entityD, int other, int other2); +DLL Slvs_Constraint Slvs_Coincident(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, + Slvs_Entity workplane); +DLL Slvs_Constraint Slvs_Distance(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, double value, + Slvs_Entity workplane); +DLL Slvs_Constraint Slvs_Equal(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, + Slvs_Entity workplane); +DLL Slvs_Constraint Slvs_EqualAngle(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, Slvs_Entity entityC, + Slvs_Entity entityD, + Slvs_Entity workplane); +DLL Slvs_Constraint Slvs_EqualPointToLine(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, + Slvs_Entity entityC, Slvs_Entity entityD, + Slvs_Entity workplane); +DLL Slvs_Constraint Slvs_Ratio(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, double value, + Slvs_Entity workplane); +DLL Slvs_Constraint Slvs_Symmetric(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, + Slvs_Entity entityC , + Slvs_Entity workplane); +DLL Slvs_Constraint Slvs_SymmetricH(uint32_t grouph, Slvs_Entity ptA, Slvs_Entity ptB, + Slvs_Entity workplane); +DLL Slvs_Constraint Slvs_SymmetricV(uint32_t grouph, Slvs_Entity ptA, Slvs_Entity ptB, + Slvs_Entity workplane); +DLL Slvs_Constraint Slvs_Midpoint(uint32_t grouph, Slvs_Entity ptA, Slvs_Entity ptB, + Slvs_Entity workplane); +DLL Slvs_Constraint Slvs_Horizontal(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity workplane, + Slvs_Entity entityB); +DLL Slvs_Constraint Slvs_Vertical(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity workplane, + Slvs_Entity entityB); +DLL Slvs_Constraint Slvs_Diameter(uint32_t grouph, Slvs_Entity entityA, double value); +DLL Slvs_Constraint Slvs_SameOrientation(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB); +DLL Slvs_Constraint Slvs_Angle(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, double value, + Slvs_Entity workplane, + int inverse); +DLL Slvs_Constraint Slvs_Perpendicular(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, + Slvs_Entity workplane, + int inverse); +DLL Slvs_Constraint Slvs_Parallel(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, + Slvs_Entity workplane); +DLL Slvs_Constraint Slvs_Tangent(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, + Slvs_Entity workplane); +DLL Slvs_Constraint Slvs_DistanceProj(uint32_t grouph, Slvs_Entity ptA, Slvs_Entity ptB, double value); +DLL Slvs_Constraint Slvs_LengthDiff(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, double value, + Slvs_Entity workplane); +DLL Slvs_Constraint Slvs_Dragged(uint32_t grouph, Slvs_Entity ptA, Slvs_Entity workplane); + +DLL double Slvs_GetParamValue(uint32_t ph); +DLL void Slvs_SetParamValue(uint32_t ph, double value); + +DLL void Slvs_Solve(Slvs_System *sys, uint32_t hg); +DLL void Slvs_MarkDragged(Slvs_Entity ptA); +/** + * Setting `bad` to a non NULL pointer enables finding of bad constraints. + * If such constraints are found, `bad` is set to a heap allocated array containing + * the handles of those constraints, while the `nbad` member of the returned + * `Slvs_SolveResult` struct is set to the number of the bad constraints. + * NOTE: the user is responsible for freeing the heap allocated memory using `free()`. + */ +DLL Slvs_SolveResult Slvs_SolveSketch(uint32_t hg, Slvs_hConstraint **bad); +DLL void Slvs_ClearSketch(); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/js/README.md b/js/README.md new file mode 100644 index 0000000..e1e900c --- /dev/null +++ b/js/README.md @@ -0,0 +1,84 @@ +# slvs + +SolveSpace's geometric constraint solver for the browser and node.js + +## example + +```html + + +``` \ No newline at end of file diff --git a/js/slvs.d.ts b/js/slvs.d.ts new file mode 100644 index 0000000..6c10376 --- /dev/null +++ b/js/slvs.d.ts @@ -0,0 +1,194 @@ +export interface Entity { + h: number; + group: number; + type: number; + wrkpl: number; + normal: number; + distance: number; + param: [number, number, number, number]; +} + +export interface Constraint { + h: number; + group: number; + type: number; + wrkpl: Entity; + valA: number; + ptA: Entity; + ptB: Entity; + entityA: Entity; + entityB: Entity; + entityC: Entity; + entityD: Entity; + other: boolean; + other2: boolean; +} + +export interface SolveResult { + result: number; + dof: number; + nbad: number; + bad: Uint32Array; +} + +export interface Vector { + x: number + y: number + z: number +} + +export interface Quaternion { + w: number + vx: number + vy: number + vz: number + plus(b: Quaternion): Quaternion; + minus(b: Quaternion): Quaternion; + scaledBy(s: number): Quaternion; + magnitude(): number; + withMagnitude(s: number): Quaternion; + rotationU(): Vector; + rotationV(): Vector; + rotationN(): Vector; + rotate(p: Vector): Vector; + toThe(p: number): Quaternion; + inverse(): Quaternion; + times(b: Quaternion): Quaternion; + mirror(): Quaternion; +} + +export interface QuaternionConstructor { + new(): Quaternion; + prototype: Quaternion; + from(w: number, vx: number, vy: number, vz: number): Quaternion; +} + +export interface SlvsModule { + C_POINTS_COINCIDENT: 100000; + C_PT_PT_DISTANCE: 100001; + C_PT_PLANE_DISTANCE: 100002; + C_PT_LINE_DISTANCE: 100003; + C_PT_FACE_DISTANCE: 100004; + C_PT_IN_PLANE: 100005; + C_PT_ON_LINE: 100006; + C_PT_ON_FACE: 100007; + C_EQUAL_LENGTH_LINES: 100008; + C_LENGTH_RATIO: 100009; + C_EQ_LEN_PT_LINE_D: 100010; + C_EQ_PT_LN_DISTANCES: 100011; + C_EQUAL_ANGLE: 100012; + C_EQUAL_LINE_ARC_LEN: 100013; + C_SYMMETRIC: 100014; + C_SYMMETRIC_HORIZ: 100015; + C_SYMMETRIC_VERT: 100016; + C_SYMMETRIC_LINE: 100017; + C_AT_MIDPOINT: 100018; + C_HORIZONTAL: 100019; + C_VERTICAL: 100020; + C_DIAMETER: 100021; + C_PT_ON_CIRCLE: 100022; + C_SAME_ORIENTATION: 100023; + C_ANGLE: 100024; + C_PARALLEL: 100025; + C_PERPENDICULAR: 100026; + C_ARC_LINE_TANGENT: 100027; + C_CUBIC_LINE_TANGENT: 100028; + C_EQUAL_RADIUS: 100029; + C_PROJ_PT_DISTANCE: 100030; + C_WHERE_DRAGGED: 100031; + C_CURVE_CURVE_TANGENT: 100032; + C_LENGTH_DIFFERENCE: 100033; + C_ARC_ARC_LEN_RATIO: 100034; + C_ARC_LINE_LEN_RATIO: 100035; + C_ARC_ARC_DIFFERENCE: 100036; + C_ARC_LINE_DIFFERENCE: 100037; + + E_POINT_IN_3D: 50000; + E_POINT_IN_2D: 50001; + E_NORMAL_IN_3D: 60000; + E_NORMAL_IN_2D: 60001; + E_DISTANCE: 70000; + E_WORKPLANE: 80000; + E_LINE_SEGMENT: 80001; + E_CUBIC: 80002; + E_CIRCLE: 80003; + E_ARC_OF_CIRCLE: 80004; + + E_NONE: Entity; + E_FREE_IN_3D: Entity; + + isFreeIn3D(entity: Entity): boolean; + is3D(entity: Entity): boolean; + isNone(entity: Entity): boolean; + isPoint2D(entity: Entity): boolean; + isPoint3D(entity: Entity): boolean; + isNormal2D(entity: Entity): boolean; + isNormal3D(entity: Entity): boolean; + isLine(entity: Entity): boolean; + isLine2D(entity: Entity): boolean; + isLine3D(entity: Entity): boolean; + isCubic(entity: Entity): boolean; + isArc(entity: Entity): boolean; + isWorkplane(entity: Entity): boolean; + isDistance(entity: Entity): boolean; + isPoint(entity: Entity): boolean; + + addPoint2D(grouph: number, u: number, v: number, workplane: Entity): Entity; + addPoint3D(grouph: number, x: number, y: number, z: number): Entity; + addNormal2D(grouph: number, workplane: Entity): Entity; + addNormal3D(grouph: number, qw: number, qx: number, qy: number, qz: number): Entity; + addDistance(grouph: number, value: number, workplane: Entity): Entity; + addLine2D(grouph: number, ptA: Entity, ptB: Entity, workplane: Entity): Entity; + addLine3D(grouph: number, ptA: Entity, ptB: Entity): Entity; + addCubic(grouph: number, ptA: Entity, ptB: Entity, ptC: Entity, ptD: Entity, workplane: Entity): Entity; + addArc(grouph: number, normal: Entity, center: Entity, start: Entity, end: Entity, workplane: Entity): Entity; + addCircle(grouph: number, normal: Entity, center: Entity, radius: Entity, workplane: Entity): Entity; + addWorkplane(grouph: number, origin: Entity, nm: Entity): Entity; + addBase2D(grouph: number): Entity; + + addConstraint( + grouph: number, + type: number, + workplane: Entity, + val: number, + ptA: Entity, + ptB: Entity, + entityA: Entity, + entityB: Entity, + entityC: Entity, + entityD: Entity, + other: boolean, + other2: boolean, + ): Constraint; + + coincident(grouph: number, entityA: Entity, entityB: Entity, workplane: Entity): Constraint; + distance(grouph: number, entityA: Entity, entityB: Entity, value: number, workplane: Entity): Constraint; + equal(grouph: number, entityA: Entity, entityB: Entity, workplane: Entity): Constraint; + equalAngle(grouph: number, entityA: Entity, entityB: Entity, entityC: Entity, entityD: Entity, workplane: Entity): Constraint; + equalPointToLine(grouph: number, entityA: Entity, entityB: Entity, entityC: Entity, entityD: Entity, workplane: Entity): Constraint; + ratio(grouph: number, entityA: Entity, entityB: Entity, value: number, workplane: Entity): Constraint; + symmetric(grouph: number, entityA: Entity, entityB: Entity, entityC: Entity): Constraint; + symmetricH(grouph: number, ptA: Entity, ptB: Entity, workplane: Entity): Constraint; + symmetricV(grouph: number, ptA: Entity, ptB: Entity, workplane: Entity): Constraint; + midpoint(grouph: number, ptA: Entity, ptB: Entity, workplane: Entity): Constraint; + horizontal(grouph: number, entityA: Entity, workplane: Entity, entityB: Entity): Constraint; + vertical(grouph: number, entityA: Entity, workplane: Entity, entityB: Entity): Constraint; + diameter(grouph: number, entityA: Entity, value: number): Constraint; + sameOrientation(grouph: number, entityA: Entity, entityB: Entity): Constraint; + angle(grouph: number, entityA: Entity, entityB: Entity, value: number, workplane: Entity, inverse: boolean): Constraint; + perpendicular(grouph: number, entityA: Entity, entityB: Entity, workplane: Entity, inverse: boolean): Constraint; + parallel(grouph: number, entityA: Entity, entityB: Entity, workplane: Entity): Constraint; + tangent(grouph: number, entityA: Entity, entityB: Entity, workplane: Entity): Constraint; + distanceProj(grouph: number, ptA: Entity, ptB: Entity, value: number): Constraint; + lengthDiff(grouph: number, entityA: Entity, entityB: Entity, value: number, workplane: Entity): Constraint; + dragged(grouph: number, ptA: Entity, workplane: Entity): Constraint; + + getParamValue(ph: number): number; + setParamValue(ph: number, value: number): number; + solveSketch(hgroup: number, calculateFaileds: boolean): SolveResult; + clearSketch(): void; +} + +declare function ModuleLoader(): Promise; + +export default ModuleLoader; diff --git a/package.json b/package.json new file mode 100644 index 0000000..c9188b7 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "slvs", + "version": "0.0.0", + "description": "\"SolveSpace", + "main": "slvs.js", + "types": "slvs.d.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/solvespace/solvespace.git" + }, + "keywords": [], + "author": "", + "license": "GNU GPL V3", + "bugs": { + "url": "https://github.com/solvespace/solvespace/issues" + }, + "homepage": "http://solvespace.com" +} diff --git a/pkg/flatpak/.gitignore b/pkg/flatpak/.gitignore new file mode 100644 index 0000000..ca1b1d7 --- /dev/null +++ b/pkg/flatpak/.gitignore @@ -0,0 +1,4 @@ +/.flatpak-builder +/build-dir +/repo +*.flatpak diff --git a/pkg/flatpak/build.sh b/pkg/flatpak/build.sh new file mode 100755 index 0000000..a65961f --- /dev/null +++ b/pkg/flatpak/build.sh @@ -0,0 +1,4 @@ +#!/bin/sh -ex +cd $(dirname $0) +flatpak-builder "$@" --force-clean --repo repo build-dir com.solvespace.SolveSpace.json +flatpak build-bundle repo solvespace.flatpak com.solvespace.SolveSpace diff --git a/pkg/flatpak/com.solvespace.SolveSpace.json b/pkg/flatpak/com.solvespace.SolveSpace.json new file mode 100644 index 0000000..005a8bc --- /dev/null +++ b/pkg/flatpak/com.solvespace.SolveSpace.json @@ -0,0 +1,267 @@ +{ + "$schema": "https://raw.githubusercontent.com/TingPing/flatpak-manifest-schema/master/flatpak-manifest.schema", + "app-id": "com.solvespace.SolveSpace", + "runtime": "org.freedesktop.Platform", + "runtime-version": "25.08", + "sdk": "org.freedesktop.Sdk", + "finish-args": [ + "--device=dri", + "--share=ipc", + "--socket=fallback-x11", + "--socket=wayland" + ], + "cleanup": [ + "/include", + "/lib/cmake", + "/lib/pkgconfig", + "/share/aclocal", + "/share/pkgconfig", + "*.la" + ], + "command": "solvespace", + "modules": [ + { + "name": "mm-common", + "buildsystem": "meson", + "sources": [ + { + "type": "archive", + "url": "https://download.gnome.org/sources/mm-common/1.0/mm-common-1.0.7.tar.xz", + "sha256": "494abfce781418259b1e9d8888c73af4de4b6f3be36cc75d9baa8baa0f2a7a39", + "x-checker-data": { + "type": "gnome", + "name": "mm-common", + "stable-only": true + } + } + ], + "cleanup": [ + "/bin", + "/share/doc", + "/share/man", + "/share/mm-common" + ] + }, + { + "name": "sigc++", + "buildsystem": "meson", + "config-opts": [ + "--buildtype=release", + "-Dbuild-examples=false" + ], + "sources": [ + { + "type": "archive", + "url": "https://download.gnome.org/sources/libsigc++/2.10/libsigc++-2.10.8.tar.xz", + "sha256": "235a40bec7346c7b82b6a8caae0456353dc06e71f14bc414bcc858af1838719a", + "x-checker-data": { + "type": "gnome", + "name": "libsigc++", + "stable-only": true, + "versions": { + "<": "3.0.0" + } + } + } + ], + "cleanup": [ + "/lib/sigc++-*" + ] + }, + { + "name": "glibmm", + "buildsystem": "meson", + "config-opts": [ + "--buildtype=release", + "-Dbuild-examples=false" + ], + "sources": [ + { + "type": "archive", + "url": "https://download.gnome.org/sources/glibmm/2.66/glibmm-2.66.8.tar.xz", + "sha256": "64f11d3b95a24e2a8d4166ecff518730f79ecc27222ef41faf7c7e0340fc9329", + "x-checker-data": { + "type": "gnome", + "name": "glibmm", + "stable-only": true, + "versions": { + "<": "2.68.0" + } + } + } + ], + "cleanup": [ + "/lib/giomm-*", + "/lib/glibmm-*", + "/lib/libglibmm_generate_extra_defs-*.so*" + ] + }, + { + "name": "cairomm", + "config-opts": [ + "--disable-documentation" + ], + "sources": [ + { + "type": "archive", + "url": "https://download.gnome.org/sources/cairomm/1.12/cairomm-1.12.0.tar.xz", + "sha256": "a54ada8394a86182525c0762e6f50db6b9212a2109280d13ec6a0b29bfd1afe6", + "x-checker-data": { + "type": "gnome", + "name": "cairomm", + "stable-only": true, + "versions": { + "<": "1.16.0" + } + } + } + ], + "cleanup": [ + "/lib/cairomm-*" + ] + }, + { + "name": "pangomm", + "buildsystem": "meson", + "config-opts": [ + "--buildtype=release" + ], + "sources": [ + { + "type": "archive", + "url": "https://download.gnome.org/sources/pangomm/2.46/pangomm-2.46.4.tar.xz", + "sha256": "b92016661526424de4b9377f1512f59781f41fb16c9c0267d6133ba1cd68db22", + "x-checker-data": { + "type": "gnome", + "name": "pangomm", + "stable-only": true, + "versions": { + "<": "2.48.0" + } + } + } + ], + "cleanup": [ + "/lib/pangomm-*" + ] + }, + { + "name": "atkmm", + "buildsystem": "meson", + "config-opts": [ + "--buildtype=release" + ], + "sources": [ + { + "type": "archive", + "url": "https://download.gnome.org/sources/atkmm/2.28/atkmm-2.28.4.tar.xz", + "sha256": "0a142a8128f83c001efb8014ee463e9a766054ef84686af953135e04d28fdab3", + "x-checker-data": { + "type": "gnome", + "name": "atkmm", + "stable-only": true, + "versions": { + "<": "2.30.0" + } + } + } + ], + "cleanup": [ + "/lib/atkmm-*" + ] + }, + { + "name": "gtkmm", + "buildsystem": "meson", + "config-opts": [ + "--buildtype=release", + "-Dbuild-demos=false", + "-Dbuild-tests=false" + ], + "sources": [ + { + "type": "archive", + "url": "https://download.gnome.org/sources/gtkmm/3.24/gtkmm-3.24.10.tar.xz", + "sha256": "7ab7e2266808716e26c39924ace1fb46da86c17ef39d989624c42314b32b5a76", + "x-checker-data": { + "type": "gnome", + "name": "gtkmm", + "stable-only": true, + "versions": { + "<": "4.0.0" + } + } + } + ], + "cleanup": [ + "/lib/gdkmm-*", + "/lib/gtkmm-*" + ] + }, + { + "name": "eigen", + "buildsystem": "cmake-ninja", + "builddir": true, + "sources": [ + { + "type": "archive", + "url": "https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.tar.gz", + "sha256": "8586084f71f9bde545ee7fa6d00288b264a2b7ac3607b974e54d13e7162c1c72", + "x-checker-data": { + "type": "html", + "url": "https://eigen.tuxfamily.org/index.php?title=Main_Page", + "version-pattern": "latest stable release is Eigen ([\\d\\.-]+[\\d])", + "stable-only": true, + "url-template": "https://gitlab.com/libeigen/eigen/-/archive/$version/eigen-$version.tar.gz" + } + } + ], + "cleanup": [ + "*" + ] + }, + { + "name": "libjson-c", + "buildsystem": "cmake-ninja", + "builddir": true, + "config-opts": [ + "-DCMAKE_BUILD_TYPE=Release", + "-DBUILD_STATIC_LIBS=OFF", + "-DENABLE_THREADING=ON", + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5" + ], + "sources": [ + { + "type": "archive", + "url": "https://s3.amazonaws.com/json-c_releases/releases/json-c-0.18.tar.gz", + "sha256": "876ab046479166b869afc6896d288183bbc0e5843f141200c677b3e8dfb11724", + "x-checker-data": { + "type": "anitya", + "project-id": 1477, + "stable-only": true, + "url-template": "https://s3.amazonaws.com/json-c_releases/releases/json-c-$version.tar.gz" + } + } + ] + }, + { + "name": "solvespace", + "buildsystem": "cmake-ninja", + "builddir": true, + "config-opts": [ + "-DCMAKE_BUILD_TYPE=RelWithDebInfo", + "-DFLATPAK=ON", + "-DENABLE_TESTS=OFF" + ], + "sources": [ + { + "type": "dir", + "path": "../.." + } + ], + "cleanup": [ + "/lib/libslvs*.so*" + ] + } + ] +} diff --git a/pkg/snap/.gitignore b/pkg/snap/.gitignore new file mode 100644 index 0000000..9f938db --- /dev/null +++ b/pkg/snap/.gitignore @@ -0,0 +1,3 @@ +*.snap +solvespace-snap-src +squashfs-root diff --git a/pkg/snap/build.sh b/pkg/snap/build.sh new file mode 100755 index 0000000..abd7a86 --- /dev/null +++ b/pkg/snap/build.sh @@ -0,0 +1,12 @@ +#!/bin/sh -xe + +dir="$(dirname "$(readlink -f "$0")")" +solvespace_snap_src="$dir/solvespace-snap-src" +trap "rm -rf $solvespace_snap_src" EXIT + +cd "$dir" + +git_root="$(git rev-parse --show-toplevel)" +rsync --filter=":- .gitignore" -r "$git_root"/ "$solvespace_snap_src" + +snapcraft "$@" diff --git a/pkg/snap/snap/snapcraft.yaml b/pkg/snap/snap/snapcraft.yaml new file mode 100644 index 0000000..7d3cf77 --- /dev/null +++ b/pkg/snap/snap/snapcraft.yaml @@ -0,0 +1,87 @@ +name: solvespace +base: core22 +summary: Parametric 2d/3d CAD +adopt-info: solvespace +description: | + SOLVESPACE is a free (GPLv3) parametric 3d CAD tool. + Applications include + * modeling 3d parts — draw with extrudes, revolves, and Boolean (union / difference) operations + * modeling 2d parts — draw the part as a single section, and export DXF, PDF, SVG; use 3d assembly to verify fit + * 3d-printed parts — export the STL or other triangle mesh expected by most 3d printers + * preparing CAM data — export 2d vector art for a waterjet machine or laser cutter; or generate STEP or STL, for import into third-party CAM software for machining + * mechanism design — use the constraint solver to simulate planar or spatial linkages, with pin, ball, or slide joints + * plane and solid geometry — replace hand-solved trigonometry and spreadsheets with a live dimensioned drawing + +confinement: strict +license: GPL-3.0 +compression: lzo +grade: stable + +layout: + /usr/share/solvespace: + symlink: $SNAP/usr/share/solvespace + +apps: + solvespace: + command: usr/bin/solvespace + desktop: solvespace.desktop + extensions: [gnome] + plugs: [opengl, unity7, home, removable-media, gsettings, network] + environment: + GTK_USE_PORTAL: "0" + cli: + command: usr/bin/solvespace-cli + extensions: [gnome] + plugs: [home, removable-media, network] + +parts: + solvespace: + plugin: cmake + source: ./solvespace-snap-src + source-type: local + override-pull: | + craftctl default + git submodule update --init extlib/libdxfrw extlib/mimalloc extlib/eigen + override-build: | + craftctl default + project_version=$(grep CMAKE_PROJECT_VERSION:STATIC CMakeCache.txt | cut -d "=" -f2) + cd $CRAFT_PART_SRC + version="$project_version~$(git rev-parse --short=8 HEAD)" + craftctl set version="$version" + cmake-parameters: + - -DCMAKE_INSTALL_PREFIX=/usr + - -DCMAKE_BUILD_TYPE=Release + - -DENABLE_TESTS=OFF + - -DSNAP=ON + - -DENABLE_OPENMP=ON + - -DENABLE_LTO=ON + build-packages: + - zlib1g-dev + - libpng-dev + - libcairo2-dev + - libfreetype6-dev + - libjson-c-dev + - libgl-dev + - libsigc++-2.0-dev + - libspnav-dev + - git + - g++ + - libc6-dev + stage-packages: + - libspnav0 + - libsigc++-2.0-0v5 + + cleanup: + after: [solvespace] + plugin: nil + build-snaps: [gnome-42-2204] + override-prime: | + set -eux + for snap in "gnome-42-2204"; do # List all content-snaps you're using here + cd "/snap/$snap/current" && find . -type f,l -exec rm -f "$CRAFT_PRIME/{}" "$CRAFT_PRIME/usr/{}" \; + done + for cruft in bug lintian man; do + rm -rf $CRAFT_PRIME/usr/share/$cruft + done + find $CRAFT_PRIME/usr/share/doc/ -type f -not -name 'copyright' -delete + find $CRAFT_PRIME/usr/share -type d -empty -delete diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..cc18b09 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,52 @@ +[build-system] +requires = ["scikit-build-core", "Cython", "cmake==3.31.6", "ninja"] +build-backend = "scikit_build_core.build" + +[project] +name = "slvs" +version = "0.0.0" +description="SolveSpace solver wrapped with Cython" +readme = "README.md" +authors = [ + { name = "Koen Schmeets", email = "koen@schmeets.de" }, +] +requires-python = ">=3.7" +classifiers = [ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", +] + +[tool.scikit-build] +wheel.expand-macos-universal-tags = true +cmake.verbose = true +cmake.build-type = "RelWithDebInfo" +build.targets = ["slvs-py"] +install.components = ["python"] +sdist.include = [ + ".git/HEAD", + ".git/refs" +] +wheel.packages = ["python/slvs"] + +[tool.scikit-build.cmake.define] +ENABLE_GUI = "OFF" +ENABLE_CLI = "OFF" +ENABLE_TESTS = "OFF" +ENABLE_COVERAGE = "OFF" +ENABLE_OPENMP = "OFF" +FORCE_VENDORED_Eigen3 = "ON" +ENABLE_LTO = "ON" +ENABLE_PYTHON_LIB = "ON" + +[tool.cibuildwheel] +build-verbosity = 1 \ No newline at end of file diff --git a/python/slvs/__init__.py b/python/slvs/__init__.py new file mode 100644 index 0000000..9595104 --- /dev/null +++ b/python/slvs/__init__.py @@ -0,0 +1,99 @@ +from .solvespace import ( + add_base_2d, + add_arc, + add_circle, + add_constraint, + add_cubic, + add_distance, + add_line_2d, + add_line_3d, + add_normal_2d, + add_normal_3d, + add_point_2d, + add_point_3d, + add_workplane, + angle, + clear_sketch, + coincident, + diameter, + distance, + distance_proj, + dragged, + equal, + equal_angle, + equal_point_to_line, + get_param_value, + set_param_value, + horizontal, + length_diff, + make_quaternion, + midpoint, + parallel, + perpendicular, + quaternion_n, + quaternion_u, + quaternion_v, + ratio, + same_orientation, + solve_sketch, + symmetric, + symmetric_h, + symmetric_v, + tangent, + vertical, + ResultFlag, + ConstraintType, + E_NONE, + E_FREE_IN_3D +) + +__all__ = [ + "add_base_2d", + "add_arc", + "add_circle", + "add_constraint", + "add_cubic", + "add_distance", + "add_line_2d", + "add_line_3d", + "add_normal_2d", + "add_normal_3d", + "add_point_2d", + "add_point_3d", + "add_workplane", + "angle", + "clear_sketch", + "coincident", + "diameter", + "distance", + "distance_proj", + "dragged", + "equal", + "equal_angle", + "equal_point_to_line", + "get_param_value", + "set_param_value", + "horizontal", + "length_diff", + "make_quaternion", + "midpoint", + "parallel", + "perpendicular", + "quaternion_n", + "quaternion_u", + "quaternion_v", + "ratio", + "same_orientation", + "solve_sketch", + "symmetric", + "symmetric_h", + "symmetric_v", + "tangent", + "vertical", + "ResultFlag", + "ConstraintType", + "E_NONE", + "E_FREE_IN_3D", +] + +__test__ = {} diff --git a/python/slvs/solvespace.pyi b/python/slvs/solvespace.pyi new file mode 100644 index 0000000..54497fc --- /dev/null +++ b/python/slvs/solvespace.pyi @@ -0,0 +1,280 @@ +# -*- coding: utf-8 -*- + +from typing import Tuple, TypedDict +from enum import IntEnum, auto + +class ConstraintType(IntEnum): + """Symbol of the constraint types.""" + POINTS_COINCIDENT = 100000 + PT_PT_DISTANCE = 100001 + PT_PLANE_DISTANCE = 100002 + PT_LINE_DISTANCE = 100003 + PT_FACE_DISTANCE = 100004 + PT_IN_PLANE = 100005 + PT_ON_LINE = 100006 + PT_ON_FACE = 100007 + EQUAL_LENGTH_LINES = 100008 + LENGTH_RATIO = 100009 + EQ_LEN_PT_LINE_D = 100010 + EQ_PT_LN_DISTANCES = 100011 + EQUAL_ANGLE = 100012 + EQUAL_LINE_ARC_LEN = 100013 + SYMMETRIC = 100014 + SYMMETRIC_HORIZ = 100015 + SYMMETRIC_VERT = 100016 + SYMMETRIC_LINE = 100017 + AT_MIDPOINT = 100018 + HORIZONTAL = 100019 + VERTICAL = 100020 + DIAMETER = 100021 + PT_ON_CIRCLE = 100022 + SAME_ORIENTATION = 100023 + ANGLE = 100024 + PARALLEL = 100025 + PERPENDICULAR = 100026 + ARC_LINE_TANGENT = 100027 + CUBIC_LINE_TANGENT = 100028 + EQUAL_RADIUS = 100029 + PROJ_PT_DISTANCE = 100030 + WHERE_DRAGGED = 100031 + CURVE_CURVE_TANGENT = 100032 + LENGTH_DIFFERENCE = 100033 + ARC_ARC_LEN_RATIO = 100034 + ARC_LINE_LEN_RATIO = 100035 + ARC_ARC_DIFFERENCE = 100036 + ARC_LINE_DIFFERENCE = 100037 + +class EntityType(IntEnum): + POINT_IN_3D = 50000 + POINT_IN_2D = 50001 + NORMAL_IN_3D = 60000 + NORMAL_IN_2D = 60001 + DISTANCE = 70000 + WORKPLANE = 80000 + LINE_SEGMENT = 80001 + CUBIC = 80002 + CIRCLE = 80003 + ARC_OF_CIRCLE = 80004 + +class ResultFlag(IntEnum): + """Symbol of the result flags.""" + OKAY = 0 + INCONSISTENT = auto() + DIDNT_CONVERGE = auto() + TOO_MANY_UNKNOWNS = auto() + REDUNDANT_OKAY = auto() + +class Slvs_Entity(TypedDict): + h: int + group: int + type: int + wrkpl: int + point: list[int] + normal: int + distance: int + param: list[int] + +class Slvs_Constraint(TypedDict): + h: int + group: int + type: int + wrkpl: int + valA: float + ptA: int + ptB: int + entityA: int + entityB: int + entityC: int + entityD: int + other: int + other2: int + +class Slvs_SolveResult(TypedDict): + result: int + dof: int + rank: int + bad: int + +E_FREE_IN_3D : Slvs_Entity +E_NONE : Slvs_Entity + +def quaternion_u( + qw: float, + qx: float, + qy: float, + qz: float +) -> Tuple[float, float, float]: + ... + +def quaternion_v( + qw: float, + qx: float, + qy: float, + qz: float +) -> Tuple[float, float, float]: + ... + +def quaternion_n( + qw: float, + qx: float, + qy: float, + qz: float +) -> Tuple[float, float, float]: + ... + +def make_quaternion( + ux: float, + uy: float, + uz: float, + vx: float, + vy: float, + vz: float +) -> Tuple[float, float, float, float]: + ... + +def add_base_2d(grouph: int) -> Slvs_Entity: + ... + +def add_point_2d(grouph: int, u: float, v: float, wp: Slvs_Entity) -> Slvs_Entity: + ... + +def add_point_3d(grouph: int, x: float, y: float, z: float) -> Slvs_Entity: + ... + +def add_normal_2d(grouph: int, wp: Slvs_Entity) -> Slvs_Entity: + ... + +def add_normal_3d(grouph: int, qw: float, qx: float, qy: float, qz: float) -> Slvs_Entity: + ... + +def add_distance(grouph: int, d: float, wp: Slvs_Entity) -> Slvs_Entity: + ... + +def add_line_2d(grouph: int, p1: Slvs_Entity, p2: Slvs_Entity, wp: Slvs_Entity) -> Slvs_Entity: + ... + +def add_line_3d(grouph: int, p1: Slvs_Entity, p2: Slvs_Entity) -> Slvs_Entity: + ... + +def add_cubic(grouph: int, p1: Slvs_Entity, p2: Slvs_Entity, p3: Slvs_Entity, p4: Slvs_Entity, wp: Slvs_Entity) -> Slvs_Entity: + ... + +def add_arc(grouph: int, nm: Slvs_Entity, ct: Slvs_Entity, start: Slvs_Entity, end: Slvs_Entity, wp: Slvs_Entity) -> Slvs_Entity: + ... + +def add_circle(grouph: int, nm: Slvs_Entity, ct: Slvs_Entity, radius: Slvs_Entity, wp: Slvs_Entity) -> Slvs_Entity: + ... + +def add_workplane(grouph: int, origin: Slvs_Entity, nm: Slvs_Entity) -> Slvs_Entity: + ... + +def add_constraint( + grouph: int, + c_type: ConstraintType, + wp: Slvs_Entity, + v: float, + p1: Slvs_Entity = E_NONE, + p2: Slvs_Entity = E_NONE, + e1: Slvs_Entity = E_NONE, + e2: Slvs_Entity = E_NONE, + e3: Slvs_Entity = E_NONE, + e4: Slvs_Entity = E_NONE, + other: int = 0, + other2: int = 0 +) -> Slvs_Constraint: + ... + +def coincident(grouph: int, e1: Slvs_Entity, e2: Slvs_Entity, wp: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + ... + +def distance( + grouph: int, + e1: Slvs_Entity, + e2: Slvs_Entity, + value: float, + wp: Slvs_Entity = E_FREE_IN_3D +) -> Slvs_Constraint: + ... + +def equal(grouph: int, e1: Slvs_Entity, e2: Slvs_Entity, wp: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + ... + +def equal_angle( + grouph: int, + e1: Slvs_Entity, + e2: Slvs_Entity, + e3: Slvs_Entity, + e4: Slvs_Entity, + wp: Slvs_Entity = E_FREE_IN_3D +) -> Slvs_Constraint: + ... + +def equal_point_to_line( + grouph: int, + e1: Slvs_Entity, + e2: Slvs_Entity, + e3: Slvs_Entity, + e4: Slvs_Entity, + wp: Slvs_Entity = E_FREE_IN_3D +) -> Slvs_Constraint: + ... + +def ratio(grouph: int, e1: Slvs_Entity, e2: Slvs_Entity, value: float, wp: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + ... + +def symmetric(grouph: int, e1: Slvs_Entity, e2: Slvs_Entity, e3: Slvs_Entity = E_NONE, wp: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + ... + +def symmetric_h(grouph: int, e1: Slvs_Entity, e2: Slvs_Entity, wp: Slvs_Entity) -> Slvs_Constraint: + ... + +def symmetric_v(grouph: int, e1: Slvs_Entity, e2: Slvs_Entity, wp: Slvs_Entity) -> Slvs_Constraint: + ... + +def midpoint(grouph: int, e1: Slvs_Entity, e2: Slvs_Entity, wp: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + ... + +def horizontal(grouph: int, e1: Slvs_Entity, wp: Slvs_Entity) -> Slvs_Constraint: + ... + +def vertical(grouph: int, e1: Slvs_Entity, wp: Slvs_Entity) -> Slvs_Constraint: + ... + +def diameter(grouph: int, e1: Slvs_Entity, value: float) -> Slvs_Constraint: + ... + +def same_orientation(grouph: int, e1: Slvs_Entity, e2: Slvs_Entity) -> Slvs_Constraint: + ... + +def angle(grouph: int, e1: Slvs_Entity, e2: Slvs_Entity, value: float, wp: Slvs_Entity = E_FREE_IN_3D, inverse: bool = False) -> Slvs_Constraint: + ... + +def perpendicular(grouph: int, e1: Slvs_Entity, e2: Slvs_Entity, wp: Slvs_Entity = E_FREE_IN_3D, inverse: bool = False) -> Slvs_Constraint: + ... + +def parallel(grouph: int, e1: Slvs_Entity, e2: Slvs_Entity, wp: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + ... + +def tangent(grouph: int, e1: Slvs_Entity, e2: Slvs_Entity, wp: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + ... + +def distance_proj(grouph: int, e1: Slvs_Entity, e2: Slvs_Entity, value: float) -> Slvs_Constraint: + ... + +def length_diff(grouph: int, Slvs_entityA: Slvs_Entity, Slvs_entityB: Slvs_Entity, value: float, workplane: Slvs_Entity) -> Slvs_Constraint: + ... + +def dragged(grouph: int, e1: Slvs_Entity, wp: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + ... + +def clear_sketch() -> None: + ... + +def solve_sketch(grouph: int, calculateFaileds: bool) -> Slvs_SolveResult: + ... + +def get_param_value(ph: int) -> float: + ... + +def set_param_value(ph: int, value: float) -> None: + ... \ No newline at end of file diff --git a/python/tests/__main__.py b/python/tests/__main__.py new file mode 100644 index 0000000..7cbb027 --- /dev/null +++ b/python/tests/__main__.py @@ -0,0 +1,4 @@ +from unittest import defaultTestLoader, TextTestRunner + +if __name__ == '__main__': + TextTestRunner().run(defaultTestLoader.discover('tests', '*.py')) diff --git a/python/tests/test.py b/python/tests/test.py new file mode 100644 index 0000000..9a37d73 --- /dev/null +++ b/python/tests/test.py @@ -0,0 +1,253 @@ +from unittest import TestCase +import slvs +# from solvespace import ConstraintType, E_NONE +from math import radians + +class CoreTest(TestCase): + def test_crank_rocker(self): + """Crank rocker example.""" + print("Crank rocker") + slvs.clear_sketch() + g = 1 + wp = slvs.add_base_2d(g) + p0 = slvs.add_point_2d(g, 0, 0, wp) + slvs.dragged(g, p0, wp) + p1 = slvs.add_point_2d(g, 90, 0, wp) + slvs.dragged(g, p1, wp) + line0 = slvs.add_line_2d(g, p0, p1, wp) + p2 = slvs.add_point_2d(g, 20, 20, wp) + p3 = slvs.add_point_2d(g, 0, 10, wp) + p4 = slvs.add_point_2d(g, 30, 20, wp) + slvs.distance(g, p2, p3, 40, wp) + slvs.distance(g, p2, p4, 40, wp) + slvs.distance(g, p3, p4, 70, wp) + slvs.distance(g, p0, p3, 35, wp) + slvs.distance(g, p1, p4, 70, wp) + line1 = slvs.add_line_2d(g, p0, p3, wp) + slvs.angle(g, line0, line1, 45, wp, False) + # slvs.add_constraint(g, ConstraintType.ANGLE, wp, 45.0, E_NONE, E_NONE, line0, line1) + + result = slvs.solve_sketch(g, False) + self.assertEqual(result['result'], slvs.ResultFlag.OKAY) + x = slvs.get_param_value(p2['param'][0]) + y = slvs.get_param_value(p2['param'][1]) + self.assertAlmostEqual(39.54852, x, 4) + self.assertAlmostEqual(61.91009, y, 4) + + def test_involute(self): + """Involute example.""" + print("Involute") + r = 10 + angle = 45 + slvs.clear_sketch() + g = 1 + wp = slvs.add_base_2d(g) + p0 = slvs.add_point_2d(g, 0, 0, wp) + slvs.dragged(g, p0, wp) + p1 = slvs.add_point_2d(g, 0, 10, wp) + slvs.distance(g, p0, p1, r, wp) + line0 = slvs.add_line_2d(g, p0, p1, wp) + + p2 = slvs.add_point_2d(g, 10, 10, wp) + line1 = slvs.add_line_2d(g, p1, p2, wp) + slvs.distance(g, p1, p2, r * radians(angle), wp) + slvs.perpendicular(g, line0, line1, wp, False) + + p3 = slvs.add_point_2d(g, 10, 0, wp) + slvs.dragged(g, p3, wp) + line_base = slvs.add_line_2d(g, p0, p3, wp) + slvs.angle(g, line0, line_base, angle, wp, False) + + result = slvs.solve_sketch(g, False) + print("result", result) + self.assertEqual(result['result'], slvs.ResultFlag.OKAY) + x = slvs.get_param_value(p2['param'][0]) + y = slvs.get_param_value(p2['param'][1]) + self.assertAlmostEqual(12.62467, x, 4) + self.assertAlmostEqual(1.51746, y, 4) + + def test_jansen_linkage(self): + """Jansen's linkage example.""" + + print("Jansen's linkage") + slvs.clear_sketch() + g = 1 + wp = slvs.add_base_2d(g) + p0 = slvs.add_point_2d(g, 0, 0, wp) + slvs.dragged(g, p0, wp) + + p1 = slvs.add_point_2d(g, 0, 20, wp) + slvs.distance(g, p0, p1, 15, wp) + line0 = slvs.add_line_2d(g, p0, p1, wp) + + p2 = slvs.add_point_2d(g, -38, -7.8, wp) + slvs.dragged(g, p2, wp) + p3 = slvs.add_point_2d(g, -50, 30, wp) + p4 = slvs.add_point_2d(g, -70, -15, wp) + slvs.distance(g, p2, p3, 41.5, wp) + slvs.distance(g, p3, p4, 55.8, wp) + slvs.distance(g, p2, p4, 40.1, wp) + + p5 = slvs.add_point_2d(g, -50, -50, wp) + p6 = slvs.add_point_2d(g, -10, -90, wp) + p7 = slvs.add_point_2d(g, -20, -40, wp) + slvs.distance(g, p5, p6, 65.7, wp) + slvs.distance(g, p6, p7, 49.0, wp) + slvs.distance(g, p5, p7, 36.7, wp) + + slvs.distance(g, p1, p3, 50, wp) + slvs.distance(g, p1, p7, 61.9, wp) + + p8 = slvs.add_point_2d(g, 20, 0, wp) + line_base = slvs.add_line_2d(g, p0, p8, wp) + slvs.angle(g, line0, line_base, 45, wp, False) + + result = slvs.solve_sketch(g, False) + print("result", result) + self.assertEqual(result['result'], slvs.ResultFlag.OKAY) + x = slvs.get_param_value(p8['param'][0]) + y = slvs.get_param_value(p8['param'][1]) + self.assertAlmostEqual(18.93036, x, 4) + self.assertAlmostEqual(13.63778, y, 4) + + def test_nut_cracker(self): + print("Nut cracker") + + h0 = 0.5 + b0 = 0.75 + r0 = 0.25 + n1 = 1.5 + n2 = 2.3 + l0 = 3.25 + + slvs.clear_sketch() + g = 1 + wp = slvs.add_base_2d(g) + p0 = slvs.add_point_2d(g, 0, 0, wp) + slvs.dragged(g, p0, wp) + + p1 = slvs.add_point_2d(g, 2, 2, wp) + p2 = slvs.add_point_2d(g, 2, 0, wp) + line0 = slvs.add_line_2d(g, p0, p2, wp) + slvs.horizontal(g, line0, wp) + line1 = slvs.add_line_2d(g, p1, p2, wp) + p3 = slvs.add_point_2d(g, b0 / 2, h0, wp) + slvs.dragged(g, p3, wp) + slvs.distance(g, p3, line1, r0, wp) + slvs.distance(g, p0, p1, n1, wp) + slvs.distance(g, p1, p2, n2, wp) + + result = slvs.solve_sketch(g, False) + print("result", result) + self.assertEqual(result['result'], slvs.ResultFlag.OKAY) + x = slvs.get_param_value(p2['param'][0]) + print("x", x) + ans_min = x - b0 / 2 + ans_max = l0 - r0 - b0 / 2 + self.assertAlmostEqual(1.01576, ans_min, 4) + self.assertAlmostEqual(2.625, ans_max, 4) + + def test_pydemo(self): + """Some sample code for slvs.dll. + We draw some geometric entities, provide initial guesses for their positions, + and then constrain them. The solver calculates their new positions, + in order to satisfy the constraints. + + Copyright 2008-2013 Jonathan Westhues. + Copyright 2016-2017 Yuan Chang [pyslvs@gmail.com] Solvespace bundled. + + An example of a constraint in 2d. In our first group, we create a workplane + along the reference frame's xy plane. In a second group, we create some + entities in that group and dimension them. + """ + + slvs.clear_sketch() + g1 = 1 + g2 = 2 + # First, we create our workplane. Its origin corresponds to the origin + # of our base frame (x y z) = (0 0 0) + p101 = slvs.add_point_3d(g1, 0, 0, 0) + # and it is parallel to the xy plane, so it has basis vectors (1 0 0) + # and (0 1 0). + (w, vx, vy, vz) = slvs.make_quaternion(1, 0, 0, 0, 1, 0) + n102 = slvs.add_normal_3d(g1, w, vx, vy, vz) + wp200 = slvs.add_workplane(g1, p101, n102) + + # Now create a second group. We'll solve group 2, while leaving group 1 + # constant; so the workplane that we've created will be locked down, + # and the solver can't move it. + p301 = slvs.add_point_2d(g2, 10, 20, wp200) + p302 = slvs.add_point_2d(g2, 20, 10, wp200) + + # And we create a line segment with those endpoints. + l400 = slvs.add_line_2d(g2, p301, p302, wp200) + + # Now three more points. + p303 = slvs.add_point_2d(g2, 100, 120, wp200) + p304 = slvs.add_point_2d(g2, 120, 110, wp200) + p305 = slvs.add_point_2d(g2, 115, 115, wp200) + + # And arc, centered at point 303, starting at point 304, ending at + # point 305. + a401 = slvs.add_arc(g2, n102, p303, p304, p305, wp200) + + # Now one more point, and a distance + p306 = slvs.add_point_2d(g2, 200, 200, wp200) + d307 = slvs.add_distance(g2, 30, wp200) + + # And a complete circle, centered at point 306 with radius equal to + # distance 307. The normal is 102, the same as our workplane. + c402 = slvs.add_circle(g2, n102, p306, d307, wp200) + + # The length of our line segment is 30.0 units. + slvs.distance(g2, p301, p302, 30, wp200) + + # And the distance from our line segment to the origin is 10.0 units. + slvs.distance(g2, p101, l400, 10, wp200) + + # And the line segment is vertical. + slvs.vertical(g2, l400, wp200) + + # And the distance from one endpoint to the origin is 15.0 units. + slvs.distance(g2, p301, p101, 15, wp200) + + # The arc and the circle have equal radius. + slvs.equal(g2, a401, c402, wp200) + + # The arc has radius 17.0 units. + slvs.diameter(g2, a401, 17 * 2) + + # If the solver fails, then aslvs it to report which constraints caused + # the problem. + + # And solve. + g1result = slvs.solve_sketch(g1, False) + g2result = slvs.solve_sketch(g2, False) + self.assertEqual(g2result['result'], slvs.ResultFlag.OKAY) + x = slvs.get_param_value(p301['param'][0]) + y = slvs.get_param_value(p301['param'][1]) + self.assertAlmostEqual(10, x, 4) + self.assertAlmostEqual(11.18030, y, 4) + x = slvs.get_param_value(p302['param'][0]) + y = slvs.get_param_value(p302['param'][1]) + self.assertAlmostEqual(10, x, 4) + self.assertAlmostEqual(-18.81966, y, 4) + x = slvs.get_param_value(p303['param'][0]) + y = slvs.get_param_value(p303['param'][1]) + self.assertAlmostEqual(101.11418, x, 4) + self.assertAlmostEqual(119.04153, y, 4) + x = slvs.get_param_value(p304['param'][0]) + y = slvs.get_param_value(p304['param'][1]) + self.assertAlmostEqual(116.47661, x, 4) + self.assertAlmostEqual(111.76171, y, 4) + x = slvs.get_param_value(p305['param'][0]) + y = slvs.get_param_value(p305['param'][1]) + self.assertAlmostEqual(117.40922, x, 4) + self.assertAlmostEqual(114.19676, y, 4) + x = slvs.get_param_value(p306['param'][0]) + y = slvs.get_param_value(p306['param'][1]) + self.assertAlmostEqual(200, x, 4) + self.assertAlmostEqual(200, y, 4) + x = slvs.get_param_value(d307['param'][0]) + self.assertAlmostEqual(17, x, 4) + self.assertEqual(6, g2result['dof']) diff --git a/res/CMakeLists.txt b/res/CMakeLists.txt new file mode 100644 index 0000000..7e1b2db --- /dev/null +++ b/res/CMakeLists.txt @@ -0,0 +1,326 @@ +# First, set up registration functions for the kinds of resources we handle. +set(resource_root ${CMAKE_CURRENT_SOURCE_DIR}/) +set(resource_list) +if(WIN32) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/win32/versioninfo.rc.in + ${CMAKE_CURRENT_BINARY_DIR}/win32/versioninfo.rc) + + set(rc_file ${CMAKE_CURRENT_BINARY_DIR}/resources.rc) + file(WRITE ${rc_file} "// Autogenerated; do not edit\n") + file(APPEND ${rc_file} "#include \n") + file(APPEND ${rc_file} "#include \"${CMAKE_CURRENT_BINARY_DIR}/win32/versioninfo.rc\"\n") + + function(add_resource name) + set(source ${CMAKE_CURRENT_SOURCE_DIR}/${name}) + + if(${ARGC} GREATER 1) + set(id ${ARGV1}) + else() + string(REPLACE ${resource_root} "" id ${source}) + endif() + if(${ARGC} GREATER 2) + set(type ${ARGV2}) + else() + set(type RCDATA) + endif() + file(SHA512 "${source}" hash) + file(APPEND ${rc_file} "${id} ${type} \"${source}\" // ${hash}\n") + # CMake doesn't track file dependencies across directories, so we force + # a reconfigure (which changes the RC file because of the hash above) + # every time a resource is changed. + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${source}") + endfunction() +elseif(APPLE) + set(app_resource_dir ${CMAKE_BINARY_DIR}/Resources) + set(cli_resource_dir ${CMAKE_BINARY_DIR}/res) + + function(add_resource name) + set(source ${CMAKE_CURRENT_SOURCE_DIR}/${name}) + set(target_app ${app_resource_dir}/${name}) + set(target_cli ${cli_resource_dir}/${name}) + set(resource_list "${resource_list};${target_app};${target_cli}" PARENT_SCOPE) + + get_filename_component(target_app_dir ${target_app} DIRECTORY) + get_filename_component(target_cli_dir ${target_cli} DIRECTORY) + add_custom_command( + OUTPUT ${target_app} ${target_cli} + COMMAND ${CMAKE_COMMAND} -E make_directory ${target_app_dir} + COMMAND ${CMAKE_COMMAND} -E copy ${source} ${target_app} + COMMAND ${CMAKE_COMMAND} -E make_directory ${target_cli_dir} + COMMAND ${CMAKE_COMMAND} -E copy ${source} ${target_cli} + COMMENT "Copying resource ${name}" + DEPENDS ${source} + VERBATIM) + endfunction() + + function(add_xib name) + set(source ${CMAKE_CURRENT_SOURCE_DIR}/${name}) + get_filename_component(basename ${name} NAME_WE) + set(target ${app_resource_dir}/${basename}.nib) + set(resource_list "${resource_list};${target}" PARENT_SCOPE) + + add_custom_command( + OUTPUT ${target} + COMMAND ${CMAKE_COMMAND} -E make_directory ${app_resource_dir} + COMMAND ibtool --errors --warnings --notices --output-format human-readable-text + --compile ${target} ${source} + COMMENT "Building Interface Builder file ${name}" + DEPENDS ${source} + VERBATIM) + endfunction() + + function(add_iconset name) + set(source ${CMAKE_CURRENT_SOURCE_DIR}/${name}) + get_filename_component(basename ${name} NAME_WE) + set(target ${app_resource_dir}/${basename}.icns) + set(resource_list "${resource_list};${target}" PARENT_SCOPE) + + add_custom_command( + OUTPUT ${target} + COMMAND ${CMAKE_COMMAND} -E make_directory ${app_resource_dir} + COMMAND iconutil -c icns -o ${target} ${source} + COMMENT "Building icon set ${name}" + DEPENDS ${source} + VERBATIM) + endfunction() +elseif(EMSCRIPTEN) + function(add_resource name) + set(source ${CMAKE_CURRENT_SOURCE_DIR}/${name}) + set(resource_list "${resource_list};${source}" PARENT_SCOPE) + endfunction() +else() # Unix + include(GNUInstallDirs) + + set(app_resource_dir ${CMAKE_BINARY_DIR}/res) + + function(add_resource name) + set(source ${CMAKE_CURRENT_SOURCE_DIR}/${name}) + set(target ${app_resource_dir}/${name}) + set(resource_list "${resource_list};${target}" PARENT_SCOPE) + + get_filename_component(target_dir ${target} DIRECTORY) + add_custom_command( + OUTPUT ${target} + COMMAND ${CMAKE_COMMAND} -E make_directory ${target_dir} + COMMAND ${CMAKE_COMMAND} -E copy ${source} ${target} + COMMENT "Copying resource ${name}" + DEPENDS ${source} + VERBATIM) + + get_filename_component(name_dir ${name} DIRECTORY) + install(FILES ${source} + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/solvespace/${name_dir}) + endfunction() +endif() + +function(add_resources) + foreach(name ${ARGN}) + add_resource(${name}) + set(resource_list "${resource_list}" PARENT_SCOPE) + endforeach() +endfunction() + +# Second, register all resources. +if(WIN32) + add_resource(win32/icon.ico 4000 ICON) + add_resource(win32/manifest.xml 1 RT_MANIFEST) +elseif(APPLE) + add_iconset (cocoa/AppIcon.iconset) + add_xib (cocoa/MainMenu.xib) + add_xib (cocoa/SaveFormatAccessory.xib) +else() + add_resource(freedesktop/solvespace-48x48.png) + + if(FLATPAK) + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/freedesktop/solvespace-flatpak.desktop.in + ${CMAKE_CURRENT_BINARY_DIR}/freedesktop/solvespace-flatpak.desktop) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/freedesktop/solvespace-flatpak.desktop + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications + RENAME com.solvespace.SolveSpace.desktop) + + set(DESKTOP_FILE_NAME com.solvespace.SolveSpace.desktop) + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/freedesktop/com.solvespace.SolveSpace.metainfo.xml.in + ${CMAKE_CURRENT_BINARY_DIR}/freedesktop/com.solvespace.SolveSpace.metainfo.xml) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/freedesktop/com.solvespace.SolveSpace.metainfo.xml + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/metainfo) + + install(FILES freedesktop/solvespace-flatpak-mime.xml + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/mime/packages + RENAME com.solvespace.SolveSpace-slvs.xml) + + install(FILES freedesktop/solvespace-scalable.svg + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps + RENAME com.solvespace.SolveSpace.svg) + install(FILES freedesktop/solvespace-scalable.svg + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/mimetypes + RENAME com.solvespace.SolveSpace.svg) + + foreach(SIZE 16x16 24x24 32x32 48x48) + install(FILES freedesktop/solvespace-${SIZE}.png + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/${SIZE}/apps + RENAME com.solvespace.SolveSpace.png) + install(FILES freedesktop/solvespace-${SIZE}.png + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/${SIZE}/mimetypes + RENAME com.solvespace.SolveSpace.png) + endforeach() + elseif(SNAP) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/freedesktop/solvespace-snap.desktop + DESTINATION / + RENAME solvespace.desktop) + + # snapd does not support registering new mime types + + install(FILES freedesktop/solvespace-scalable.svg + DESTINATION /meta/icons/hicolor/scalable/apps + RENAME snap.solvespace.svg) + + foreach(SIZE 16x16 24x24 32x32 48x48) + install(FILES freedesktop/solvespace-${SIZE}.png + DESTINATION /meta/icons/hicolor/${SIZE}/apps + RENAME snap.solvespace.png) + endforeach() + else() + if(USE_QT_GUI) + set(SS_EXE solvespace-qt) + else() + set(SS_EXE solvespace) + endif() + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/freedesktop/solvespace.desktop.in + ${CMAKE_CURRENT_BINARY_DIR}/freedesktop/${SS_EXE}.desktop) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/freedesktop/${SS_EXE}.desktop + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications) + + set(DESKTOP_FILE_NAME ${SS_EXE}.desktop) + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/freedesktop/com.solvespace.SolveSpace.metainfo.xml.in + ${CMAKE_CURRENT_BINARY_DIR}/freedesktop/com.solvespace.SolveSpace.metainfo.xml) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/freedesktop/com.solvespace.SolveSpace.metainfo.xml + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/metainfo) + + install(FILES freedesktop/solvespace-mime.xml + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/mime/packages + RENAME solvespace-slvs.xml) + + install(FILES freedesktop/solvespace.thumbnailer + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/thumbnailers + RENAME solvespace.thumbnailer) + + install(FILES freedesktop/solvespace-scalable.svg + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps + RENAME solvespace.svg) + install(FILES freedesktop/solvespace-scalable.svg + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/mimetypes + RENAME application-x-solvespace.svg) + + foreach(SIZE 16x16 24x24 32x32 48x48) + install(FILES freedesktop/solvespace-${SIZE}.png + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/${SIZE}/apps + RENAME solvespace.png) + install(FILES freedesktop/solvespace-${SIZE}.png + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/${SIZE}/mimetypes + RENAME application-x-solvespace.png) + endforeach() + endif() +endif() + +add_resources( + banner.txt + icons/graphics-window/angle.png + icons/graphics-window/arc.png + icons/graphics-window/assemble.png + icons/graphics-window/bezier.png + icons/graphics-window/circle.png + icons/graphics-window/construction.png + icons/graphics-window/equal.png + icons/graphics-window/extrude.png + icons/graphics-window/helix.png + icons/graphics-window/horiz.png + icons/graphics-window/image.png + icons/graphics-window/in3d.png + icons/graphics-window/lathe.png + icons/graphics-window/length.png + icons/graphics-window/line.png + icons/graphics-window/ontoworkplane.png + icons/graphics-window/other-supp.png + icons/graphics-window/parallel.png + icons/graphics-window/perpendicular.png + icons/graphics-window/pointonx.png + icons/graphics-window/point.png + icons/graphics-window/rectangle.png + icons/graphics-window/ref.png + icons/graphics-window/revolve.png + icons/graphics-window/same-orientation.png + icons/graphics-window/sketch-in-3d.png + icons/graphics-window/sketch-in-plane.png + icons/graphics-window/step-rotate.png + icons/graphics-window/step-translate.png + icons/graphics-window/symmetric.png + icons/graphics-window/tangent-arc.png + icons/graphics-window/text.png + icons/graphics-window/trim.png + icons/graphics-window/vert.png + icons/text-window/constraint.png + icons/text-window/constraint-dimo.png + icons/text-window/constraint-wo.png + icons/text-window/construction.png + icons/text-window/edges.png + icons/text-window/faces.png + icons/text-window/occluded-visible.png + icons/text-window/occluded-stippled.png + icons/text-window/occluded-invisible.png + icons/text-window/mesh.png + icons/text-window/normal.png + icons/text-window/outlines.png + icons/text-window/point.png + icons/text-window/shaded.png + icons/text-window/workplane.png + locales.txt + locales/cs_CZ.po + locales/de_DE.po + locales/en_US.po + locales/fr_FR.po + locales/uk_UA.po + locales/es_AR.po + locales/tr_TR.po + locales/ru_RU.po + locales/zh_CN.po + locales/ja_JP.po + fonts/unifont.hex.gz + fonts/private/0-check-false.png + fonts/private/1-check-true.png + fonts/private/2-radio-false.png + fonts/private/3-radio-true.png + fonts/private/4-stipple-dot.png + fonts/private/5-stipple-dash-long.png + fonts/private/6-stipple-dash.png + fonts/private/7-stipple-zigzag.png + fonts/unicode.lff.gz + fonts/BitstreamVeraSans-Roman-builtin.ttf + shaders/imesh.frag + shaders/imesh.vert + shaders/imesh_point.frag + shaders/imesh_point.vert + shaders/imesh_tex.frag + shaders/imesh_texa.frag + shaders/imesh_tex.vert + shaders/mesh.frag + shaders/mesh.vert + shaders/mesh_fill.frag + shaders/mesh_fill.vert + shaders/edge.frag + shaders/edge.vert + shaders/outline.vert + threejs/three-r111.min.js.gz + threejs/hammer-2.0.8.js.gz + threejs/SolveSpaceControls.js) + +# Third, distribute the resources. +add_custom_target(resources + DEPENDS ${resource_list} + SOURCES ${resource_list}) +if(WIN32) + set_property(TARGET resources PROPERTY EXTRA_SOURCES ${rc_file}) +endif() diff --git a/res/banner.txt b/res/banner.txt new file mode 100644 index 0000000..ad8f4f8 --- /dev/null +++ b/res/banner.txt @@ -0,0 +1 @@ +SolveSpace! diff --git a/res/cocoa/AppIcon.iconset/icon_128x128.png b/res/cocoa/AppIcon.iconset/icon_128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..363e89161a4804a5338624941481f15f915be2ea GIT binary patch literal 891 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU~J8Fb`B^Ga(7bjbagf}v@|o* zHPSO=V9=OYI&r7Bv!g)!eig4H%&x%^5lcG6CUv+8zO@nHcbXuf7&!aMQz4<+J>r5J zw`|zD;lqJr3TD@Cb^dQq;K)*KS{tEpqr)bBvP091$g=OZ@1HyK?p%}Yq^m_~vGz}{ zeR9jFIwaevw{xz(MaJT}c~VKUR`#Di&%Povp8v*`n)kswvwr{0_U&AQ zJs)}s3XIol+X<9q&Wv#S;dCSW1Z(U3DRTNBXFn>L_*QHAX^wWa*4RVuN<@#ZjyNr0 z`A{*@UvKxqsqYuZU78aU9TTwYi=K~@k8$Jd$YnE9W-z*?${cH-ZxkWkeW5@1f(`rE_0zuIkRkS(2GyZ=2#PW;ZwPrnMi7J2aY1g8o=O1M8Th0GELr~j`@8!1;F1vFJ zRk>u#FYJg}67`xANvMrhh;7ef;fleTVh>=y-} zOJvVnHey8hF0b=FYffqZU(I)K*FT8gttgD--)T2k@9gG^&HDOoe*^0bq+l?= zZrrkA>xK^pjwzU3yVd!>L4hMnwP|gH#*Ge}^vMoQGa}2r-@bqD%)4_ruWct#mN_%R>4(#e>=Uf5^QXw^f1LfOWa3+`<)=B?)mmc@y(Oms}Zt}l8%PCmwsvm=+yNSVRtmMU|seZE~}pz^)=9{x>w z8)x-cMN6!iq~kL6{FTR)GLb&YC zEmY-_Ex)iMW>s|M{6oqu+Z7AA+ubiH|68lQ|JmDg9p4kv4g%wav%n*=n1Mk|6@(c* zgH%2+Ffdzqx;TbZ%z1lvWA32<5r;tGi-%a&JmfSgSNkC4!@xYjOh{;=Xh-fl%dOr0 z!m)cF*-L<;7$10GzI!SE2B+;a&sVqSTswY=one|XgMb?C(YE+s3^}KN7VJD*`LD$G zU0kWyzjG`MiA;D=8DquD%vot~`Fb8_ls}N4$)Mmfq)^WGFN|R?%s&5|zVGnoheqFR zYzlVC{XPFbfvLfO5ik0_=>6I(@q~4mv)=I8T(sz0{p+{gY)*zlEO^nqU-Ld2@Em^r zbi=CMALEa|zP-PA+hKyC{Cg?C!I}SZ|JJUqjgyNmF+W03NA?ZYim$ORw%*!AdjTM>r>mdKI;Vst0D>lteE4?>U_AyJ#; z~msgKXk2dHUZ57^Xv?^C&I`~7Q8iKMB4twDH9 z(~Z3FjBs!>uPZ(fc4?VdLij@1YsdwKpOhXi@tfjwk?ks{%Ls=igGo6#KF*;f3LSUQxLm4R zd7r`~gtsAZ3x`M0BmS)`zvk^{EKSIk!2kdN32;bRa{vGf6951U69E94oEQKA0BcD^ zK~y-6V_-l9{~1UGOr&ZU7z3X0e!}TBqFr$M^l7Yy43RLPsR4{={Qod9d?7FMnGdcH zrvVHMxI>@~ literal 0 HcmV?d00001 diff --git a/res/cocoa/AppIcon.iconset/icon_16x16@2x.png b/res/cocoa/AppIcon.iconset/icon_16x16@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d9f276ab5d34eab271ff6461b9b46f67e273e372 GIT binary patch literal 626 zcmV-&0*(ENP)4?>U_AyJ#; z~msgKXk2dHUZ57^Xv?^C&I`~7Q8iKMB4twDH9 z(~Z3FjBs!>uPZ(fc4?VdLij@1YsdwKpOhXi@tfjwk?ks{%Ls=igGo6#KF*;f3LSUQxLm4R zd7r`~gtsAZ3x`M0BmS)`zvk^{EKSIk!2kdN32;bRa{vGqB>(^xB>_oNB=7(L0Fy~X zK~z|UV_?7y{zEZM0TZn)7}C%m8jKjHmEa;_OZApirXPoMsO`t)g{Lt^-a zz^DU89Wd&EA?E-_JaYfBsUfGlXJBApkonAq)vloz0x&>Id5;WCO-)hE8+;+){D~K< z&-U_t#ur*-g8>f#76#(m{xs<$42lpyk6T zZrrkA>xK^pjwzU3yVd!>L4hMnwP|gH#*Ge}^vMoQGa}2r-@bqD%)4_ruWct#mN_%R>4(#e>=Uf5^QXw^f1LfOWa3+`<)=B?)mmc@y(Oms}Zt}l8%PCmwsvm=+yNSVRtmMU|seZE~}pz^)=9{x>w z8)x-cMN6!iq~kL6{FTR)GLb&YC zEmY-_Ex)iMW>s|M{6oqu+Z7AA+ubiH|68lQ|JmDg9p4kv4g%wav%n*=n1O*?7=#%a zX3ddcU|_cJba4!+nDh4T#@s^zA`XGV7Z0(ldB|y0uJ%F7hk<#5nUK&#(T?1AmRq~~ zg=6#JHs?(1_3qNqiylO7;;YkEZBLr@?VMV zySP%Zf9F^j5}EL#GRBIPnX}T~^7TB0#kzlBVP1<(fhSo;tA_AXT9OGxoFY1`qyu}*_;fASn#5Izvg{5;5q#K z>4sIiKgJ(_eS3fLw!;KN`S((OgERl-{;geI8z&cCVt$06j_ez(6<=dtY`wep`)-Cc wZ3NwQ$9unddHDhBl+8X=NNM=eH%RWo0rN>LGA+IJ_X0p%Pgg&ebxsLQ0DgXr;s5{u literal 0 HcmV?d00001 diff --git a/res/cocoa/AppIcon.iconset/icon_256x256@2x.png b/res/cocoa/AppIcon.iconset/icon_256x256@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a380eb77900e8f23c7929e12ccc6ccb5e3a04271 GIT binary patch literal 2506 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4YzZe)8TQi-V1B!#(ofJG>oy`m_ z&CGO-^b8poG$xi#-0AJ?DA2xN#p?*OYj8xwk`A#+9d3eeZ3Ot8CP*j-&VKS#NT_y? zxZuVu8@6uvaNwAN*|l4p{~HuIvQ(SaMrhpVut}fn&@>~m?ECHe=gz!4*JL~CYEfFO z{gZ2-+%l>T$#&}PoU3nSCAVFm^P6$S28c59x{*22+vA3W**!nP_#zkHv0|1af#cw9CAZ|#H3_x-oM|L>o1 znw6%xazXA_raAX!*xZ-?)BV^Yt-ikE*1q@u_UoUay|p!G&;Kr6bMN7qy1R1sEDq%F zplw-H61n#PXU|n`gQ@q$e@q5?TYD!REX-Lg{$tU`s>aFHU+;@2aEa5lXx(;|yPzuh rQ%$}=0wayBqJ2d%fORIw>WW+HS)b)t8XT6q0C7EC{an^LB{Ts5!S&mP literal 0 HcmV?d00001 diff --git a/res/cocoa/AppIcon.iconset/icon_32x32.png b/res/cocoa/AppIcon.iconset/icon_32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..60bf2c5ff1b13145db0f776c3d63831217be1716 GIT binary patch literal 626 zcmV-&0*(ENP)4?>U_AyJ#; z~msgKXk2dHUZ57^Xv?^C&I`~7Q8iKMB4twDH9 z(~Z3FjBs!>uPZ(fc4?VdLij@1YsdwKpOhXi@tfjwk?ks{%Ls=igGo6#KF*;f3LSUQxLm4R zd7r`~gtsAZ3x`M0BmS)`zvk^{EKSIk!2kdN32;bRa{vGf6951U69E94oEQKA0Fy~X zK~z|UV_?7y{zEZM0TZn)7}C%m8jKjHmEa;_OZApirXPoMsO`t)g{Lt^-a zz^DU89Wd&EA?E-_JaYfBsUfGlXJBApkonAq)vloz0x&>Id5;WCO-)hE8+;+){D~K< z&-U_t#ur*-g8>f#76#(m{xs<$42lpyk6Tr5J zw`|zD;lqJr3TD@Cb^dQq;K)*KS{tEpqr)bBvP091$g=OZ@1HyK?p%}Yq^m_~vGz}{ zeR9jFIwaevw{xz(MaJT}c~VKUR`#Di&%Povp8v*`n)kswvwr{0_U&AQ zJs)}s3XIol+X<9q&Wv#S;dCSW1Z(U3DRTNBXFn>L_*QHAX^wWa*4RVuN<@#ZjyNr0 z`A{*@UvKxqsqYuZU78aU9TTwYi=K~@k8$Jd$YnE9W-z*?${cH-ZxkWkeW5@1f(`rE_0zuIkRkS(2GyZ=2#PW;ZwPrnMi7J2aY1g8o=O1M8Th0GELr~j`@8!1;F1vFJ zRk>u#FYJg}6V2`J~#)X6nH}}_Ro!-U5 zvBlT&{+sgMkL~piH;XMWWYBd;W{7Gy#;}E-F&QAZk1hj z%X|iq;Pl(fMaK$b-g0U#-T#jBq51)29c&9&Gk6@hFGw_?8hLi!=dFwKTRs*4yESE> zJBK|ZNa3pYjFvww>vOl=Q-+#7`}KFzuddnOY|m`}Rc7&r1!T<2S@&Q63iR81PF@uz x@V2_~lIh#p19J_4t}gyndEhG4M3~i)3~uwKn_udc{s)E?gQu&X%Q~loCIBVCLj?c; literal 0 HcmV?d00001 diff --git a/res/cocoa/AppIcon.iconset/icon_512x512.png b/res/cocoa/AppIcon.iconset/icon_512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..6dcc757da086429df0ae3aaac7b1715b76071472 GIT binary patch literal 2506 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4YzZe)8TQi-V1B!#(ofJG>oy`m_ z&CGO-^b8poG$xi#-0AJ?DA2xN#p?*OYj8xwk`A#+9d3eeZ3Ot8CP*j-&VKS#NT_y? zxZuVu8@6uvaNwAN*|l4p{~HuIvQ(SaMrhpVut}fn&@>~m?ECHe=gz!4*JL~CYEfFO z{gZ2-+%l>T$#&}PoU3n{y3wXRfh)p%!V`t`%~SqY zv+}TA;rcOSyE)LP0R#>8$$a=h1y#wP qYVrjV7-?)3?JJ4_tTRDYSKLz1`YgxN;IQNci0kR<=d#Wzp$P!xzuM{m literal 0 HcmV?d00001 diff --git a/res/cocoa/AppIcon.iconset/icon_512x512@2x.png b/res/cocoa/AppIcon.iconset/icon_512x512@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d45152055356772603ddc7ebaf94642a690617d6 GIT binary patch literal 6490 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&zE~RK2WrGXn!-Yo@bvKyi?}lY*zKvzei# znVGJUo*@H+#>CQzJH4G91={zkcpYJO4UULd(jhjf!%gt5jR3#X1PR5!*-xGd3Dxcq z7u>jI!`2NS4jfZ3yLPMde}e)?mTJ@52#p&ZHtCZcnr1|neZPJG+?jXhnrtUsElP{E ze{$`UTSnC(*-pKkbM-AU7SGL-N}9E@|NMFO6`Aq;H?GvY58j#e`){^y=aQ9;YaJ_m zF8A*F&{I%gyk6T*pe%D{gwqeF8`&pVTjx)a)Bia8QOU%&TFXy!w5zqo9(q?IdVF=n zX$i}Rii!SuyBAJp4qQ`GZfp79BnRAdBB>{?{3T+FpDwzlCtw zom;5NC0l-BN6f0|%K3+sTed3}aJRc(Q2w`8d;hby={mkArX2*v3ul2xWHAGSm?{V} zdIqU{U|*m{7xg+Dpa*E8kvb=KI9hCf}`-O@uot@KwBM}S?p00i_>zopr0149x)Bpeg literal 0 HcmV?d00001 diff --git a/res/cocoa/MainMenu.xib b/res/cocoa/MainMenu.xib new file mode 100644 index 0000000..38db0d9 --- /dev/null +++ b/res/cocoa/MainMenu.xib @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/cocoa/SaveFormatAccessory.xib b/res/cocoa/SaveFormatAccessory.xib new file mode 100644 index 0000000..3baf4c6 --- /dev/null +++ b/res/cocoa/SaveFormatAccessory.xib @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/fonts/BitstreamVeraSans-Roman-builtin.ttf b/res/fonts/BitstreamVeraSans-Roman-builtin.ttf new file mode 100644 index 0000000000000000000000000000000000000000..550fe3648afc587e9c687ced270a5ee3d300f186 GIT binary patch literal 54524 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

?tm?LU=BiO$ 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 literal 0 HcmV?d00001 diff --git a/res/fonts/private/0-check-false.png b/res/fonts/private/0-check-false.png new file mode 100644 index 0000000000000000000000000000000000000000..b9020c33b8c437c8617a537d3b03c173e421b2f0 GIT binary patch literal 218 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl%OyKpTSt fL*frHg#!$+84N28+$Y-tZDH_q^>bP0l+XkK*)B9y literal 0 HcmV?d00001 diff --git a/res/fonts/private/1-check-true.png b/res/fonts/private/1-check-true.png new file mode 100644 index 0000000000000000000000000000000000000000..7f6a42c554193d2abbd2b4a8023009fb8864be24 GIT binary patch literal 243 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLlS!3>*qfc7zXy85}S Ib4q9e0Hf$QhX4Qo literal 0 HcmV?d00001 diff --git a/res/fonts/private/3-radio-true.png b/res/fonts/private/3-radio-true.png new file mode 100644 index 0000000000000000000000000000000000000000..5322f587264a18b394550c53b8803bc5038d9612 GIT binary patch literal 231 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl#%q#+J&w=`!859mMBwb+G8Fuk* Q570^mPgg&ebxsLQ0A3F|lmGw# literal 0 HcmV?d00001 diff --git a/res/fonts/private/4-stipple-dot.png b/res/fonts/private/4-stipple-dot.png new file mode 100644 index 0000000000000000000000000000000000000000..52fc21951037b0598c36114f0556b5de17103530 GIT binary patch literal 936 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

2fXkU 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
kgPFRcz;SDYW;Up^c?kF$Bs%b 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

3hxKvp3 z;IqgpvjfusM8H((ua&Mr-RVBl#2{G8&u!zq8(N=lPh?|wdvU^VhCo%1uSRJslhpZ4 zPwikfk5JM_gI!2n6Wwx@_L0*3F9NEL>&#!pAQn8vL#3xP%j3KPzB|_n-GG^@zED6W zYpML>fdH+;wb7xt%@i8kVqJNBEmHH26P&?8n z+PC{XcHn!Ee%|_%Z*&S`qIPPONh(^Qf!K@*YB!JG6N#PO+Q*GnnWr>^EmX=^B0vqO z{{yy&SiI{@;${M6)6gSIG$7U!^vA(ye|h9GkK{&&%X!4V7&lXKvH?&R3^7C zMpr1NQU-LWg(ZkH9EZQck1Zk%P1=W+X%fl|x7gT0wa`o?-lCE$jQ-z^zYXtxzuY&k z36-r`uPoo4kK~MuAIf=T-dP9|-C8zPs*X&MkvGsYnd{rjJ_D)pti;>kjNPF@cJ8*S zr7FQf)&LU4+cJ;h_7wYO5Fa88t?salxKBzdrH~jeVfvu;?M1AJo*CReijq>(zOp&= zs5GMm?OR@f02;{|FyJBFX!@+EOrM`xwmO!92Wv1PjLTFY!fajJ4S=8HIh2w$0Hrs4 z?xmFkyU~~ARv7l%EroUH9RkSIA%Vt%TCtck#ol63%!5&yP}w=>6Qrb80GccEf_3lp zDpQ7JpQ`Wh ztH;DZ{=kslkW>SQ<(q)87VK$fkP$Lx68#c`hUAo96+PQ>G)L^{loU7JANi`zl_uZG zJvA|cbSm+@p#VM4h-Gr8M>OFBvag6WCV@Wzpp(>E@bI5eqir8Lzz z07oH=(4y*T#4WR%?i9=4A!4yvs0Dy(FPLdSB|E0JripsiwpXb^F(Doj$pSOZlcM+< zWM%?cwEIi#&>LNGfBBYh9zES+tE$>0R!L0HsB_Rf6 zIAGEU%t>my)Z<0BGcq1M+x%gbB>5SchVr&Ywm;6 zV?qG|G$K()cU9W<;femxit=|-Uy>Rl9!)%|zpYIL&!|MSUa@yuA``rnY?sV~9l>7vir!P5L%$FA-fN1okWjh6TSEi^ITyxW z2CD!EyqJc0l?KT&X*VNXD=uqy{*MvJ6F1<{gLOt%ss>RTH);Y`@g z?5XHIX875tpiU}Gs_?3L=G@71p|>&wVCQdu%NcG?yJR!nx;lO3xEn=3FY{CQ}jIjm{4 zW-KJ`Du^p0Py&Oa2K#LAx3!zV8dA~)+kIdQdYam0EJZ`(sHTbZQ)f{!vIN>wH`ABG ztDipt(K3)1pjRw895fvV%3ns3>GO9SiwCXziGd>Jp=@LNY501fF&(;0)A!yk7xWS^ zE%`c|Na*Tg5oeus>`2RxfbU3-Sq6w$i{XVQ@zxAg*d9a4$&I+k75{ivjGsZZDF_)Zwo|7tcUh4JkMm>vD3~lulBJ^HP7;GJ~XZJvM|yKLbPhbo>8oGn?VT z_U2R_4(-bCJHdhLKF86PgxaFgC66ah7pFh7eaC&>+Z=Jp+0#hR zGOH9qbn%KH^>Z>O*T74Mq7fLNfW<%JA`>;Ys;VO zFv|Memf6edLBke4z^>7G%X}e|kcH7&FkbyZCT&E9asNF`a|-5}VhRmp7}p&fc|Cus zL8f6)Ci>1q`P)>637QrMe1*%mf$O>*E%!sABnfj_QvwSPG1YIHGlnA}t>0eXH&1Vt z8PcuCZfUC$3A_)@u?l;G3Zd?Om?N1{N;(@RTRR;Cqc!ZX{1)IH)k0|SI^fd6BI2?T zzYo7BX8Dz*;@45BrLH#FCYKgMhdei>>=w;(Fg%(`v){9_+Tn^EXD&uzV504MYZXYo z2PZFaeq^!8JpCN|JcU`(x$!+oj5PNA>EZB6H7)-uuKC}oMGeKfk6LeUN2+bq;uOG> z9Q&h4oHNNt;||wTHl(+ilDQ7ArZMar>v;D*5@=3*PS4^k*16r9gb6 zOseaHvTXX(9l=SYg-R?7BXPo&88ZWZ9jPTRAZGkIlryOiGw<&M?m} zWA`XK(4+a)*@Q3!6}g}u7eSke}46p`@g@?cF=M zWP9z1VCzu(Mn`Q?5OI7P3h(p5E5qC(+vt8XOL2(>PM124<|O2bHSDQEb=U?CpBpD8 zlySI-E47HW031+P$|6Oe1rV<>k{@9yjw}49jZ~k4lnlN$7JHhhROj<%g`owYXmj@* zlS6->O2`}J9ST$WOXVeoAW!p6MIF9vy~Y9^V3rtj#QZ}C9G9*Limvdf2Blh)xy}}c zcSA9t-5&|~j825l0=&O(TZyh*OU(*XUbunW0C#J{>@La>pgaw}Km@zglLLjR+O^L3 z4<50OyuAG`?r)bUK#7xOav4QYJ`UjrZTH0it@X=P2Sd{p`s9Z8T{Cvc1r?ngUZS~P zi5EIZ2wO2cea-+gxXiB>2wVG9NR99zBdyksHJL99VI`yB7M(~5C~bX{`>0> zBac-|`R()T4A&@i-K9R6BUWf4O)q$=(8xPt5yxwc>aThW+U?s#PyuQJQH_$NQ2n93 zNWnRE-asVk8erh^U8Uus#rsBT^?%+swsygx;p!wcbVoy|FvTlbi1vrA)riRuwb=PU z4BwnOwb(?_S`_{O`i)e{UdON3D$9|fShCJO?$B6KJu?UnYTG!L4P2GERb|={SbJo- zjvVH$3dUDXqHz31w*VcO)^n{*A0E*bn?zgI^dtt%_w82nFd~P&Wto{lCNq&B;NIPw z8Hud0c_^&w>krfVdfRYQXq9av^9VxZaOw#G(MA@g-HAZ8x>7l0^13||`IV|6VHsh* z1;n96SRgwS>xcf8#v9OF`gO|;=Y}$q+C`kKrLEFK$=PNZBjivrgEyEAK1odSMD?kn zZkQvg;%Rb2jJ{7&R0TZ45p^@|=+Y7^RlZ+^(p?7_E;=$-MGfEU_d891GH_9PsM<7f zUqswij(De=b-7rPz~^pw+{+=6G#|e}4w-YQU!$m|)v_r3x=~YcB}R*N!^5q_ZL}^r z^+_?P*w+fjjo`Hm?Fkzr^tlN*S=umT0EDBgbH2OeiRDSuV6YH4|5K;~9Vtu%Cz#_b z>pZh(Az}A(MMhrkp|<&>=zg`~jXbb%e=9pVPTqj(mLAh99xhYL{oo z-Qg_uXWj~uEqPK-W|p~YPLwBe$cN=TXvB$9+@?(Rsy#Kfj4{Quq@wcc?MaytT2`Nc z5p;iS;%Iagz_Wr>0L}rew{UJ%Ak>jTu~kK9GoqNkPqOxg=+`^{EVatD8K^(X@0;2M zMLVq(5u@Zi)T4o*T>e+`<^)59^wD3E9T zt@dCCBYXP0n^mE^|Jz-)x1vE1bWcO6ju8R}H)G_BR+mD0U$YgM4i-+sADup8;(e?B ze5(fw7>j9f%3nj+BSG+YBxR0<=64ijIODy>b2Rfd&2j zy3y4s%%)LipgXi&f!{PUnOZjb9NsB1wQ3(4 zx1tm`(HrMzp%hOW$Ez3OnMO0$zzz9dZ!ejXyojx($!|^wINYL^w3|+`myuao_SGw$ad}Vbsu{AJWfDHcGfYW50 z%v(q9&E{{lI0wnpfE#td`a^?nelBXol>RV0of60`6Xf_g04vj1N33g^uGU87+)62*K78{s z{_{VruSgA+DPoQ>^`sgd!t3OM4UHsu!=V{K0DH2EV(vP>5^Fv6sx5~{=(^_LVOU7= z5BuT8kYXcdBtwgjJAPDme_y8_|1pI!R>hQpGM)^>$M`$Vtk z@N+Ok5y3`HmnzE6*4Xba!jzEOi9>_Ko4&QK|Pnhb}W@8PYKIH6^=@_n#rj8?(k2}$noaQ z1bxj2d#ac1Wg(7y-~QkGMrbZNw8A7X7;su;$s^7#s>Vb}KMbDiW)Ya^4J^9B1vD#c zo(E_Z2&(sF@?OEGB@Uk_qK-^H_xt00TRZAuRR@L573T6=fv$1D(QwrDu*_RfzC--V zlM*DWd(fVL7{D;b3_Q$BLXNK4_zK`vU?UN004;P=d>j7ShNp>F_3(kRc;CxJLyntH z>1cy4g>OVl`9ew+o}{QmGmg51Fr4j|ysqpDGnjD%jO{ClA{A zOvbBfM4_D{7({l8nMPfH4Nw`-{`#P+4D&SsjiWCV&nr*^`x7K`^-x+Xs8T1HS=w>t z!JYmn?Kopx$JspHs58PNcCl~^POm7V40o-vK&KTP#VjSwD(9SX-s94}X&($Hdedq} z!>>C|)B1|i-lrRFo+4f$0zZYijPXJ!!ojlryI1N_rT-R2reEfk|2o%te+Uj`yic z9Lk*^$96!hT2QC6jsywt=N+~cYPgVynJ#cZX)yL9Vv~MU4{+8=9rZRj4oVqjBC;}k zB}`4KqX5O#oUg)EWE50NxFTEn(H~yLURu(4ViQu4Wg-S}X-t@OJGQ16ASqvhEDm_% z4!u+nU#;pqt~m@4;6o(?-UlZaHkog#i6DCveiQ0(M8O54d~HgIq(UMa88%2QW6vkutI!9XG+|07?ISQDwbOjd8xvuoda9k;3PBJbBdQ1~n zCSF@X3`~!DX;k=U0t;1cTB~Fx&bN>ANdugM{PfuHDrqn=E}=Itf_rPBqm_C1(=fuy z86`%IQb^A!F|6FMJcJyK>bA{~)}ELqKi0mo0~d6y0nmwW`l^0$q5;t&GeI=j$#WVI zxaIXr{3>L6i@3$lHK42`|MB(?Z$ad|&&1ZukinPpRPi63WCUTgM+N~9eo-mtKtGO$ z9Aq)|e?00n4QPWcmZMRjMs4on*ZYHdWdk4i(1toV-v>yOt{YzXq5H+6L1qFz(K$<%^MW(@sN@pMz|x^NnIkoAsCpt=JcA1z zwRA=cbYHWy@<=0#a_+c(W>m<9RHuN?0YJN5LmJ>yml%hU3|_l9nl&*jiD?~4RA%dZbA+vk4$s;MSLyppy4J< z1S*3<^s@0esaXWqxT6hCi2eRpRfQlbacIJhw4vMTpdZ@>nOJrlsv;;UCm~$;W)~U7 zz2|M7hUGrurDuRycESHZ!~OO8Gfh22OrU|zfCiGOP`@P3!3_$Wx|B)p*3N}HtkML2 zFjWE$PdE<82KE^ahiO_vv~!YAPA&0TdmwuOsAkHFP7f3T=`i_z&{;JED0Re;{vkGt z?u1n41gyJ)tR|8LU;+;Tlz>XIZ|#{O^4Qv60nOgu(TKE(qL2o4w_ge;i zN^@MK$EbvR%MU%Ajlx~|Q`-qk6)$TiHmy~YTJDi;;PM?dpThi0i zCCAm+uQm^6T}1_;$cSM_{pyoOnbqfH7WoO9V094(omd#G)k6u$4YSJ)l&kK?bZ%Q< zXxcEHL8yQ&Ky}(Sfa64IhXxp=ZF#16G2Dg1d!=vQs?upfOvXWQcXv~KX5ITtk%E^G z9NfE3in%Q#D^;7ho|t=Jg6MnPXF?VfS}9K&0@P?n5J+1z5SL9{qvYS9O9BDq`(R(_ zry$L=YzfHWc3<@R**L+cBLALpp-yQXiUc%rM-7w58$X9(gy+KzJY++WN&Rx-TW5TPt+kAoasq1g!h07QjCP=JVo458arbp%@$Kht@4JqiOXY`-t% z*K_i!%277y00d!p7p`WI^6hb2nh~Vscwb~P6sKyXa@uJgNDoZiUcDjY(|qiB3sYo1 z9$mPZJjVQ|_%D^pXz4rd(?aF%_Okvi&$q$rH|f)5cs@g$1w>r=Vtn~L0Yr=70uySz=6 z6|UkilQA%_En-(^uKDo(6lF5}>s7!=3vczxHK9lOe)^tonO;=ZB~>Gt_@Oei-+Wq0 z{jQd+xZ_5HJhZ~kE&G2Z@xkM?%oWf4LZB1`xQlEO2x?qf3k;i)v6LAliOE@d``ERi z5t>r%Q|tNj?oD#}Ku<=q@fz4T=^QVN>hM{Ol?;!gyo@+bjD!zrU-mZo@k#_$ z)rtC1SD&e_uY|`VWwX>p!a(Nd>)zq}${!F6>GPB=sdFQcMRg(~T}`CWG=&4v1{lVC z-0c2F?RV5`*0g$RcHO^Vx}Lwm-pos7S=kP14DoVuzWeYPt#)jW0C-OE+)>` z2s%Xx7$tzp9Mc_VWI#Kid~&(7J2tK7_RekVC_K7mDqVevoonW2+l@y>{QcUGn)z8$q>f8J#mgF1wVEaJx$*B*KIITAK}W z6V`kqSvCv5MNDw-il7P+YMBfxz{W-t!z7WTR&#D?SVV@y+6#IYaY}Ma=(?~~?LLE} z!ZqwKB$9EOCr|#`fg0Ru#|f8<`>0@@DsAuy53+8lVd7dsR&YlL&4Ld_KF3{)QqmKm z579+rjLZ?IsZ10fh^wBNoZ+0eA<5s-<7UuSxmqgPeHQI@3FKC>0T!^Ybn{SfT)+#162mYNDsKfD>Fn8Pz3Wrh08HCIzk~@*Ro6Uh>zer-g_Yp;$b*n zIxKGpdFoS=@?7vHg;}x97|uv@|G>qV%vCvvve0dC^ay>)Pr$Kd7!@g_2l*Q&-Hw1_ zLoQo&axnc_aucTL2n|5)xpSYc=7T-RkeM9eUdVXBuqN3NLj2)p?4DGZhSIX`-#4+U zdmj40Rlx;xE7N|^q~sn>c0&(N+#qg1l$Bb1(4}-n($VoQD%bSMwir21~B?om>oxQf@|}QL7slrQA?R4igfIRUq<{<>oh=)2S#Nxf~6R zq5~lO8tuxS0-CHK-;L~dCPlT}s$1$h`@8bUPcJexIo3(~`J~ADdY|vNR4Fv=MgBF# zHr}bvpZ&E9Y`$DO93a$&Iztngu3Jt)=p_85g|`)e7B7Nl&$; zolNRGBMTbnZMh@Bt9cTSmH_od8 z%+ke3gpBn|qKR@?g!3fCMG^(f0 zZy1TNYow57;V_^55_E?~K?_82J?Q_yiO4HMx#H5nDgzNvlL3l+AMc;#B|U4?Wj7LK zY?)QoXIis@^=)Bqk~@olb*1a^k0f}P(uNBbk^9cC3IH2ddP zapFL5wj1$Hf7?Eveu!b!WumaT@C5Hc9D8!ceLACB%oP?rVxgt@eXd3ZfRJvBH}$Wd zXI%~$``32MAtw+2KCtFFjL2%dUC@s@+EwzFh?^jDAc0z>IqO(yh%%`{L#6CFNGxxO4aeUCmK6!t8+CBCP zPAqo0&^lgdh*_f{;KrR`i8qlLQ~pz{qywR*0vbb7AfiUSEJG9Y+%A9RW)PB*D%hb~x-M|lfe-Rc}F zkn)l2KZP07_GlooHXk;%d$2;6Vk0%K&T5`RnQ}YMeV2u?Gx=0c?&jF)@^)yNQzSCg zr%gw8>}M6}$it9ruAvYlQ?PwGA=U{u@b0@fNz@5}utPtCY#2EK-KW?9tvD1ak3FdA zt~gJNK{eAa17U&z zAHaqNZiGHbmTiQyMhUr=d~T$E%k3SgicD`m)H4xc;?MB-0%$tT?Apnb{GI7=*V2xb z#g#MmlO~%AX*dRC`*dzdWjOBJ9`T-%d}5x1dtwCg1R>K>8!a1HCvmvWBh;!?8KMh} zHc6Z!Y68(aR_U;;<0iGID5-|d!s!K|G>=+661WmJ*Nb`kd%5Fs5qV)#Pn0=lP;7<( zJwU?0AWrR8i1ih+Imote?t*#solg_xLI4VhJI3uSVhP6bk2NfL54xAxd>`$ z2&ulx$i@sVH%EC}m-kog%IARP1cdS+<%#vxJs{)G>Jjw6JsTHhNb%CDfy(f~l_Hzv z(-KCO4fFDtZ$%7xa? zQQ}Jq9>}=N{iTH4m-hr!p>D#&F86!;-uiakVT0@OPCd!Q6)(|S_d`0?p!Y^Tj$|81 z#1TNdP5e0Qb=5-`YW)~ik_AcrQ$v6Ok$q6oBqwGm|+zcL7j3xB*(6EmPU;N)BcjTg-0L3 z+|(0eh85)^e4;{E)xyA`PbzbhsQNV7*@RcND&?zasW`XR=BKd^-CXH@kzX%BD=*wq z(lmu^D;8_F4dM}w1dPiUntZ;x1C}$?_szoGBMUQA(P8t|jS1EJRk0v9yADa)($a<< zV_8O*opgIHr*?-7btKY9Fb|m;g0FqZgEkGBN{d30jOQ<2+LWWF@d=Xu2>W*}uz)=I zrB%?;4rQ4YN>dIx4$r-*!(@W$Bb z%l8ELYOE3qc)ja;%-$-^q|_ggDcSLgGCT$D&L`ezLhwmFG`K3z?JB5hKCDia{mNyQ z9cUK4aKf6mWSK+Y{`ImRi1E)`<4&tpzcI)Qk$2N7DSEz0SynC*!A~*f&rnsmc?x=m6qL|zOo#frpOOd>QoIuJhXud zor8j+=D=krP?GWz#YyNJoTitcd~{>wQaRvZmT|kCudf z$8FhSn?p1C>b(1!+3_}03J|V1`9{BF2Mb0^Doa10?nAirXOF51qNP+g+5;ob%oBf> zOEaEJ$k1G0&SK5nq^2$oi?7pbP@h}f7fR6Sg!}Ng4}b^6 zxK~9otv_O>qMT&FYS)+EpS9h7F zCE3>=QBx85B;_s07m{U}d?Uo$6xc`D-VPOXBPSt`3ObLZOy?!&s)rpOMSvQUn}ysv zuuP_9H^z4&X$u-$^wf0-Ho?81(F?*vbR^L3dWM=(rxca>gho`mT#KFWcXSZ?4ISPC zQ9#}_++Gvc1RX&=>Xjz7UPU7C)|j!T>&yj*W*n^8D^wZV7i_07Uh;bB+@y(zxw@s* zP3Yu1A>?Gr+S|84U^`*Zba|e=kq(R6X;&lND$KV=Uup{~B-$1aSe=I!D6n zd#E-5i<&>pbq&M>JTx~m7_vvQ)YJ5XihQe~SFF2{$uD6M{F0I{{k(3}t zQpN|}3dGoUi^RBF8Za0xz*5qps)Y?#x6jyy3((IMs>8r0ffP;7mCpeEXeBUxXAP3` zfJVYaBT#|vr&Fvwb=twa#h!#&b1Wk`Qz6!gLRSUZX$U|Jla(;u4XY>;2-Ll}Y%skH zaNUzcU%yT-qX4@r#%iL*aG!?uE!R_C+-z#VcW>FUt`(&}yn)O()`f1+sq+FFD+mGY zOJ1;eY5fJMptsT-@SS!IlbT)Ik~|LquhbueBNJ8Oq$U~~vb5nE?)&hfT?xxWtTZ!g zOLqBpwZuxB+g3$0NX5A%4464MWeiP_kp1DmIXjc&MtUO(@B1vYLqq{6F#0T7{5{?O z2#)4^{34SHnzA>R)v_fP3P|MehCrPrLr=8@$b;HtZmwK!wjlBgA<$*Kb2qQ$fCU80 z#V3hNFm?XOV6$vOO3$$gTohL=+r{#1C2#Ghet6p*t_A1TIR#}ce9V@JjL_5y@VVp$bkkSQY;VdHvKK()m7u<8 zlHiEqWT^P?_)vKYR$wx#IDH7z%pkkI>Ys^Me>T2)(?HMe*+L@2YskZmtBe4ZT%RzX z=Hhl4CFrpkHqj6SoO{(mRIwpUlAwzW|Lkx|KR0uP<70P2kBu=dBJkM#`am(Xz{=h$ zKMKfbVv)N76uk#{(8=Zgkcfr&5ZzZPW@B#~N7qM>z*}hSdB_Jt)Z?)j??}_SIiWeU zJVN$|Y0;CF4f4=fo|wsF1wevF3bwy_nqGPslAXqG(!G@>SV(}+0REE#x#j8?pJ(XV z+cR@zz7{KCT8`jkQ&#HC$)%seFnwmy6th2Qeb7rwX$)v$=efEfI1FmCXm=YQ?#o@*Gz2<2D zd^r)~Koi9Np`Nqvx+*y8Xk1v(;RWIk8az~96D7T6AV1awTC5D^N0X-j`4e;>D;o$m zuknI4FDhDRT7>Zx%c%p2R%i`rTzU}dkSoEUDgYf`ho0pHorNV_G^Hko*dGMHtC)q- z#%s>F9U!pg&>N*yddzng;Vq~qO>R!tW)E(uwlSQgp>#B9pAL?55r-FeJS3u%+_KxM zLK+9wkA<-La*Eox|9pcV=Z=#%V{8Mxc8I$k;4@|%k|j+SIvq#2g}b(523oWZ^sozX z9T#X$CA6{=!oTHmoriP?53|hT3B)eFzTXTU8gytE?bKtvsJdQT;?ad5caL3%g;CdF z#@sTU9zyD_|1dFBH9Fy&dk~)TEZ+*AfiPoTdK~%c7HB_OeS{9`)*!u3mtoPpvrN6` z9yWL<#h68=afDKEGW7rfXxWq=`xgJzX4YL4=d+Ged(gHAqD(!igies-TOcwTLo`%z z*1QB)~&C_J-@ZUdYoIatno57|@n^t9iY8On}B-BAX!&pUD89B>R^U^Y&;2a>vKF=#JEqYwiNJvF@ z6{o^pLVHRJ@VB7$yJ{X1H*fq7_SDr2U@QG^osT34CD^lceiCys4Os z<9Ojrf=)wBQYOEJV*^Mt*UY`@QO+pv@O?OzBW>1LsWT*sT1&F_uE)K8D(mp?_mxcy zRp!U7iv3wswB(%opdF~|O=X^_HFgka_tz!rftA&|f!Dw>(DMV^!7ekR&a)ZPCTmh$ z?}78?wm198#Bev1v|>eD*+)Tl64_@@!9F)a91VOAp@&MDeB0Z6 zNZ<*$o=@jh)iVt^J=Y8K-rO=-i)}qRmqv zBqgbnpy4X9?v}c3ln0J}@QbW&MN@DBG%)Z+PyXMT(>U@sH^-l}`sb7XL$)ixw%KU= ziBc#{)r8n>!5#8I$@VD1jL(dTLGkX_8SvAC*qxi2&F?}-kwAgz0{5to9+1Us|CaF6 zz!mfrLqm^Ytxcc1sRm1;Wi|XV%qrlp+9(D&mn&qe#m>=iM%DSl1gMc%KNja!>&w#o z>(9!(W=qW6oks_PRR}Z@AXf2-!qhTV{&cWna%O{7`x2JpPUTvP;fzaOe)A7bW&bC0 zgLA`w@4!_1f+l@aJY?v_`g)%*7^|I$ZgxaICX_5#!IB2ix`YXx(5tyOsNKO1__6H@ z#Zyn6pq5X*iH>{1*$Vip^fM>74)7BbJN$I0YYPgJZlp6v(uQW|UMZ0tXA_Y$LGAj_ zEf6dAw+ql}2E&6DZF33?+Za2fiCy?4XR;#+o;W+M96zkSkmf2_F@2~l9BUbl<$jx> zyjp-?g(X>3flpy-i>f|{sU!W-lgfTCD*H33?AM&^v=$U)I(XMXEzrCG%G=5@M3Vf zh83j~B7Z!)3oaZGrUTNvxXk*a#7sdgd5QO&C&0^9i?!1-U8q){7uSyNhA?wI@QYtc zG6;Ep_mi(>K`j80%Xtn`CL(3Gjee6$7i_qa*{j7lP!n$f06sppQ;ok7Qe`SoHN^}$ z(yeX7XwgDgiP+bHF^XNHsCC8Qw+C%4@^K8A3Mv7(euo`3AXptluW|k4f1z_(?I?WzkFV?(T1wFJ)uI3sf)X=ym8-rdH6F*8r+)_6X=R`pOf04=FE&r0NwnqwflM@CRnL;Z3{~hF`WlBw`(Z6CDNRciqPf}`Hv}92MV2@}e98?u_DV$AL zK+m!PZl2)=~jr4@nFWt%+r^A9H?zbdbf2 zk_SdJrt`e3`MOhO`USwRskB|1XB+YnfzeT{NX3v!UCt#94z z^z2XN{o0KdXUqwtB<(N>_G3#b)Kl|4+gY{$v0^{bxL!BuV#3L?=frn@bE>-)pQ}Lb@c)lZo$mBw3$KZ#@?hg%brE?_5K6+ zP2mIOB{Nx(Ji}D3+zlKdR1o~yZ=Z$*JGsp#oTkjd`IZ2XbV*c_hJZ1N?Wh0t=|b%4 zd(GjW!=cv-R4~ugeKnwc{NzJcCgvkt)ip$%BoqU)#g=@uplE7s{(0AxpD819tY(=s z83c}hgv*oGK+Y^&n<0UhLF-7<-`r6%@Do|!!&kkL;&hZCuEFu?2Kx78)pjjKrdk4l zmoH&^xVF&4l$XhQD zl4jy^o;t%;vV^1#lzxExZTjl1f~g__G%a z^vFb2oy*}VPkH`~Kj^XyVvj@BCzzc|ZKDyRb!3gn ztxoF$U;|BR*v`mIBu@{I30LsTpdr7sDA%vh8aooJ^EIjxA@RgA;k-nK!{%1j8nDOt)v4fl!>q45nY

i|9Lk8F!s2S+&sw`^ja)SPJW?_0F^x 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%0DU-sQG+_2c^tb1a5Gtq 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

7V`NK&b@aR)kgdUaQe&gG(A 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%0A?V^*7tk(U_RwHTT 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

=H3`VRqaSmgn)WP25nq3J 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

~ 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 free (GPLv3) parametric 3d CAD tool + + CC0-1.0 + GPL-3.0-or-later + rylie_AT_ryliepavlik.com + The SolveSpace Community, originally by Jonathan Westhues and other authors + + + Graphics + 3DGraphics + Engineering + + +

+ SolveSpace is a free (GPLv3) parametric 2d/3d CAD (computer-aided design) tool. Applications include: +

+
    +
  • Modeling 3d parts — draw with extrudes, revolves/helix, and Boolean operations
  • +
  • Modeling 2d parts — draw the part as a single section, and export; use 3d assembly to verify fit
  • +
  • Modeling 3d-printed parts — export the STL or other triangle mesh expected by most slicers
  • +
  • Preparing 2D CAM data — export 2d vector art for a waterjet machine or laser cutter
  • +
  • Mechanism design — use the constraint solver to simulate planar or spatial linkages
  • +
  • Plane and solid geometry — replace hand-solved trigonometry with a live dimensioned drawing
  • +
+
+ https://solvespace.com + https://github.com/solvespace/solvespace/issues + + @DESKTOP_FILE_NAME@ + + + + Main window with an empty document + https://solvespace.com/pics/window-linux-main.png + + + Property Browser with an empty document + https://solvespace.com/pics/window-linux-property-browser.png + + + Viewing and editing constraints on a model + https://solvespace.com/pics/front-page-pic.png + + + 3D view of a stand made from notched angle iron, from the "ex-stand" project + https://solvespace.com/pics/ex-stand-detail.jpg + + + Dimensioning a 2D sketch for a case for a printed circuit board, from the "ex-case" project + https://solvespace.com/pics/ex-case-outline.png + + + Showing tracing of Chebyshev's linkage, from the "ex-chebyshev" project + https://solvespace.com/pics/ex-chebyshev.png + + + + + application/x-solvespace + + + + + + + +

Major new stable release. Includes new arc and line length ratio and difference + constraints, comments associated with point entities. Adds "exploded view" to sketches, + and support for displaying measurements in "feet-inches". Adds a pitch parameter to + helix extrusions. Allows use of Point and Normal to define a new workplane to sketch in. + Adds live updating of Property Browser while dragging the sketch, and active links for + all points, normals, and vectors in the Property Browser. Adds the ability to link STL + files into a model. Includes a variety of UI improvements. Speeds up complex sketches + by up to 8x and doubles the maximum unknowns.

+
+ https://github.com/solvespace/solvespace/releases/tag/v3.0 +
+ + +

Major new stable release. Includes new intersection boolean operation, + new types of groups, solid model suppression, usability improvements + (especially regarding redundant constraints and automatic constraints), + and more. Also includes performance and scalability improvements.

+
+ https://github.com/solvespace/solvespace/releases/tag/v3.0 +
+ + + +

Second release candidate for the 3.0 stable release.

+
+ https://github.com/solvespace/solvespace/releases/tag/v3.0.rc2 +
+ + + +

First release candidate for the 3.0 stable release.

+
+ https://github.com/solvespace/solvespace/releases/tag/v3.0.rc1 +
+ + + +

Bug-fix release in the 2.x series, fixing some crashes.

+
+ https://github.com/solvespace/solvespace/releases/tag/v2.3 +
+ + + +

Bug-fix release, including performance improvements.

+
+ https://github.com/solvespace/solvespace/releases/tag/v2.2 +
+ + + +

Introduced *nix compatibility, internationalization, technical drawing mode, improved import and export, and other features and fixes.

+
+ https://github.com/solvespace/solvespace/releases/tag/v2.1 +
+
+ + diff --git a/res/freedesktop/solvespace-16x16.png b/res/freedesktop/solvespace-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..2d2b23f0c4f23fe50350821f4133fed7bfb9ca43 GIT binary patch literal 336 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPF2OC7#SFv>KYj98X1Hb8e5qfSeY1W8yH&|7;KPN g5kk?Bo1c=IR*74~L8m$EfEpM)UHx3vIVCg!0ByBi4*&oF literal 0 HcmV?d00001 diff --git a/res/freedesktop/solvespace-24x24.png b/res/freedesktop/solvespace-24x24.png new file mode 100644 index 0000000000000000000000000000000000000000..7ba03eed8c58e3fbb291f8ae089bb3a2be9a82a2 GIT binary patch literal 338 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaN3?zjj6;1;wmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5ln@H=32_C|KyZ)Y+Ue7$ z<*qXP|Nno@tR37yA;yv*zhDN3XE)M7oFs2|7uFy7FOLH`>?NMQuI%@jMVUmz7b?Zy z1_~K?x;TbNTux3<;5QIDo9OgVBH&nHP*6}q@r>Ty-i0#a>@9Pf^!l0<8Z?`aFI>XJ z!2XIwewzEDBS5{XC9V-ADTyViR>?)FK#IZ0z|d6Jz*yJFAjHtv%GAKh#8}(F*vi0Q igS?6miiX_$l+3hB+!_u#%~=Q3z~JfX=d#Wzp$Pyu|6K_H literal 0 HcmV?d00001 diff --git a/res/freedesktop/solvespace-32x32.png b/res/freedesktop/solvespace-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..a24bf6a8472c7c7b2cbbe0f3d72f09759ff6def4 GIT binary patch literal 348 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfvmUKs7M+SzC{oH>NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fgAr#;f;tHgJ;2y)Z)2C0% zU1j+H|Noj?;(M5ynj9D%Sq_LO zRdP`(kYX@0Ff`RQFxE9P2r)FaGBvO= qG1fLPwlXl-Ag>~Xq9HdwB{QuOw}yjGbJhVhFnGH9xvXI{(rPmO*bPE`X1*QT2iEIb=-Hu6r;dHCp&)1;OK3IR?6 zj2zw*^4LRdJED_TFf**$#4|hnmaRC@B-Ikvh?11Vl2ohYqEsNoU}Ruus%v1ZYh(~& wXl!L_U}a*gZD4F=V6Z`6MF>ShZhlH;S|x4`2c71u18QLKboFyt=akR{0Omwt`Tzg` literal 0 HcmV?d00001 diff --git a/res/freedesktop/solvespace-flatpak-mime.xml b/res/freedesktop/solvespace-flatpak-mime.xml new file mode 100644 index 0000000..760799d --- /dev/null +++ b/res/freedesktop/solvespace-flatpak-mime.xml @@ -0,0 +1,8 @@ + + + + SolveSpace model + + + + diff --git a/res/freedesktop/solvespace-flatpak.desktop.in b/res/freedesktop/solvespace-flatpak.desktop.in new file mode 100644 index 0000000..f07f293 --- /dev/null +++ b/res/freedesktop/solvespace-flatpak.desktop.in @@ -0,0 +1,10 @@ +[Desktop Entry] +Version=1.0 +Name=SolveSpace +Comment=A parametric 2d/3d CAD +Exec=${CMAKE_INSTALL_FULL_BINDIR}/solvespace %f +MimeType=application/x-solvespace +Icon=com.solvespace.SolveSpace +Type=Application +Categories=Graphics;3DGraphics;Engineering; +Keywords=parametric;cad;2d;3d; diff --git a/res/freedesktop/solvespace-mime.xml b/res/freedesktop/solvespace-mime.xml new file mode 100644 index 0000000..8ee132c --- /dev/null +++ b/res/freedesktop/solvespace-mime.xml @@ -0,0 +1,8 @@ + + + + SolveSpace model + + + + diff --git a/res/freedesktop/solvespace-scalable.svg b/res/freedesktop/solvespace-scalable.svg new file mode 100644 index 0000000..0d060d1 --- /dev/null +++ b/res/freedesktop/solvespace-scalable.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/res/freedesktop/solvespace-snap.desktop b/res/freedesktop/solvespace-snap.desktop new file mode 100644 index 0000000..7beceb0 --- /dev/null +++ b/res/freedesktop/solvespace-snap.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Version=1.0 +Name=SolveSpace +Comment=A parametric 2d/3d CAD +Exec=solvespace %f +MimeType=application/x-solvespace +Icon=${SNAP}/meta/icons/hicolor/scalable/apps/snap.solvespace.svg +Type=Application +Categories=Graphics;3DGraphics;Engineering; +Keywords=parametric;cad;2d;3d; diff --git a/res/freedesktop/solvespace.desktop.in b/res/freedesktop/solvespace.desktop.in new file mode 100644 index 0000000..a8e4db7 --- /dev/null +++ b/res/freedesktop/solvespace.desktop.in @@ -0,0 +1,10 @@ +[Desktop Entry] +Version=1.0 +Name=SolveSpace +Comment=A parametric 2d/3d CAD +Exec=${CMAKE_INSTALL_FULL_BINDIR}/${SS_EXE} %f +MimeType=application/x-solvespace +Icon=solvespace +Type=Application +Categories=Graphics;3DGraphics;Engineering; +Keywords=parametric;cad;2d;3d; diff --git a/res/freedesktop/solvespace.thumbnailer b/res/freedesktop/solvespace.thumbnailer new file mode 100644 index 0000000..2b84c75 --- /dev/null +++ b/res/freedesktop/solvespace.thumbnailer @@ -0,0 +1,4 @@ +[Thumbnailer Entry] +TryExec=solvespace-cli +Exec=solvespace-cli thumbnail --view isometric --size %sx%s --output %o %i +MimeType=application/x-solvespace diff --git a/res/icons/graphics-window/angle.png b/res/icons/graphics-window/angle.png new file mode 100644 index 0000000000000000000000000000000000000000..8b82ba3da047060117606a1b535682569d037374 GIT binary patch literal 258 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oJ@;PZ!4!i_=>}ZVMhz;1E7Pao2yAyPb7QmUe|6TwK%0@4?~j<>=;H zVt4ML-;~7yQ!lcr=S>W*Z57D(xVTH+@ciVWq?RI%bYI_B+V{P3=Lz;?Wi6=?yvTBH zMYf$waZ}=_^{Th$C}$q=kTsW?Ib9;|TTJE3DTd)QvThpLi3OyZTvl8CE&SeO)@Sbp x_s?&Xy|(sN-r-eQV%MG)Pci!yx@T(rcJ8LT(R@`pAzwgV@pScbS?83{1OPjpU77#@ literal 0 HcmV?d00001 diff --git a/res/icons/graphics-window/arc.png b/res/icons/graphics-window/arc.png new file mode 100644 index 0000000000000000000000000000000000000000..0acded45c3fe11d494cf9e0da7b4ba9e9d4937b9 GIT binary patch literal 218 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oFXxr;B5V#p$!5j(i6cI2Nb*-})ba;NB6wHyd6w85LZfJTWLMS?gjJ zx0|m^mdS&~;uEJGwOC$$!7i=jmhg@zx8F^=ylu+=ry;%3>?gMW&0|asPd>ybJMGy; zqqOJ6Q#EG_37Wc3)Gu@s3fj5O=E;H6YzzmMp8MqSa^F8!KlKbx- zXD6w>Vz>4@cks#~lMm+{FQ0I7cqpl|jnl3*mdKI;Vst0I_62SpWb4 literal 0 HcmV?d00001 diff --git a/res/icons/graphics-window/construction.png b/res/icons/graphics-window/construction.png new file mode 100644 index 0000000000000000000000000000000000000000..6f48aedefec91ee1c151344e7bdd01d63c000c6a GIT binary patch literal 203 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oFY7r;B5V#p$!bj(i6cIGR)aAN|*VvAE3autLDy9gUYR1iAbZdo|r~ zS(w8y?$j`zfK}_Sv+ca_tN6HWw%6qq7aN{k$(eue`1Sk6W^b=ed7StmdKI;Vst0G*6VqW}N^ literal 0 HcmV?d00001 diff --git a/res/icons/graphics-window/equal.png b/res/icons/graphics-window/equal.png new file mode 100644 index 0000000000000000000000000000000000000000..ba9053aad7bc5a68b1f2b43109f4e1cad9e4d17b GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oJ^TPZ!4!i_>SPc=I(V@VHp_x_|noe!;kxTUkseo_8a+^MXA-$Ac#B zXZ32oy*1*s!e6h6=QL)g{HSB7I_;$w9%*%WZkO9b1&yM$4huin=-qbQHOJMd~6zwC**8Ec+oZr%MRu;$g5XA4fq+FD=iFIQ2T z8XA-*eZyZca^B5gTe~DWM4fk3&@d literal 0 HcmV?d00001 diff --git a/res/icons/graphics-window/helix.png b/res/icons/graphics-window/helix.png new file mode 100644 index 0000000000000000000000000000000000000000..ac4f50962444c828f2033862f633ecfb84a29ded GIT binary patch literal 243 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oJ^rPZ!4!i_=%9IPx7(;9%L^nSa#&V`PCiDbhllqN@7UimrY7@GwYJQ_Wbu-m%q;xwjA82%sMruR^DmR zJ+%yu!(R2uhwpQpo8>IkpSFcnD(~)2?fUZ?S0;9tEWc)TW3xh`B-w(H!6s_6P hbJoEkG4<~qRTBUJ literal 0 HcmV?d00001 diff --git a/res/icons/graphics-window/horiz.png b/res/icons/graphics-window/horiz.png new file mode 100644 index 0000000000000000000000000000000000000000..3c23d67767d4529ba4b8f26c81766a99af2480c0 GIT binary patch literal 161 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oHuqr;B5V#p&b(3D(2hf6{;Sw|BG6SdprIl_yE@WVy0&51V{KNK_P(^Qt75Yb|Gbyy9E-l?TngPui8DL<^9e&p?>q!;C1`uGahocU~K`MIaD`A&<*>#9C!{<=Lg TE;HRK0J+xF)z4*}Q$iB}ukuvi literal 0 HcmV?d00001 diff --git a/res/icons/graphics-window/lathe.png b/res/icons/graphics-window/lathe.png new file mode 100644 index 0000000000000000000000000000000000000000..cb91631b2b7def64147005c0dba90b65a294f899 GIT binary patch literal 260 zcmV+f0sH=mP)}WH~Mwgyu0X`yIW6wl5mqv#5R`5F9L|Zq~>(K@MZr^weyI&a74_^*0ThzMnrnmo5V!!|)qPOjSb{~{mIJ%!PXtN!Z%0000< KMNUMnLSTXuJ#)zb literal 0 HcmV?d00001 diff --git a/res/icons/graphics-window/length.png b/res/icons/graphics-window/length.png new file mode 100644 index 0000000000000000000000000000000000000000..552c87c6243f2cb62c39fdf7ecae150caad119ed GIT binary patch literal 223 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oFYEr;B5V#p$;po_q%sI9MJ}{`Ft{i~koDNrB!aaygeQPZ}9HOnG6l zVP4CUof5`Z?z^`hQJtN0yhQDnVThBc;&IX0!EIj+S6MbLx@r|0@A&XyhS>3N=d&xi zJKeeevOGH-r8q4%=v8&#@@?C`OYfXrs(RC5OTI4WlUdysoWCX>VqVu={9O8>IM7WD N44$rjF6*2Ung9$iPFer} literal 0 HcmV?d00001 diff --git a/res/icons/graphics-window/line.png b/res/icons/graphics-window/line.png new file mode 100644 index 0000000000000000000000000000000000000000..356130f98bb798eefc0b3e254ee20d0a95648835 GIT binary patch literal 153 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oHusr;B5V#p&dP1wsi+Jibdx9BygsjM7^)%kfF`|NsB{XBlOkTA{gk rS-?dRu_r>4CrPwsa)X<5jMP{rSAJ6H4`OvK)1S2P zvwP?xBY~bpE!X)Ru1O`HTb8`QVUp06R5Nb|hfV6%nP2!e$X2s#KeRt|vP_L(?yFj! z2Wi)|G7ji=UAoTBrXr>K?(UL|t**DuS}~bk@U7jbF-vRTt`8Nb9?WU*x~8xG za5Gq`08}AR666<*uEUJ=@L`}FXMsm#F#`j)FbFd;%$ftT*}&7qF+}2Wa>4=;1ISgeB^>bP0 Hl+XkKzd10y literal 0 HcmV?d00001 diff --git a/res/icons/graphics-window/point.png b/res/icons/graphics-window/point.png new file mode 100644 index 0000000000000000000000000000000000000000..a1a56660c3454a37e47343c6aba8a54a5f65f02b GIT binary patch literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oHuRr;B5V#p&b(3D(640*r1olhO@bd|kUFe$B{} SDPRdw&*16m=d#Wzp$Pyeh#VyV literal 0 HcmV?d00001 diff --git a/res/icons/graphics-window/pointonx.png b/res/icons/graphics-window/pointonx.png new file mode 100644 index 0000000000000000000000000000000000000000..0fa950659253f0c27bb6e3684b10a7e17b4298d9 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!3+mLu(x9OE}#f!fk$L9 z0|U1(2s1Lwngi0a+SA1`#NzbPsg8Vy6?j;DrzF>1W}mUV`utzNDan(YRk<%ZG-aF4 z5jeCsHPTr~uuI~z{?W>lRued-IUvB$@?@HORM;Y3!<=n5ckcft z_UP8)ORap1R-Smpc)@7W;^4nNYZwE+%vh9FURikRtIFzopr0Gl*rCjbBd literal 0 HcmV?d00001 diff --git a/res/icons/graphics-window/rectangle.png b/res/icons/graphics-window/rectangle.png new file mode 100644 index 0000000000000000000000000000000000000000..3f9ea7cceea3cda0495728c48f8e3125e6d0bdb3 GIT binary patch literal 159 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oHu^r;B5V#p&b(3Dy<{_+N;FWOm6;nVK(V~!DcALTd+&y vvQQI4B)9NZrs^vT0-N28btld&6M4YIa4ITJJZ|>-IFLP_u6{1-oD!MQ*W!9;#DvbJy R6MulL_H^}gS?83{1ORZdHo5=+ literal 0 HcmV?d00001 diff --git a/res/icons/graphics-window/revolve.png b/res/icons/graphics-window/revolve.png new file mode 100644 index 0000000000000000000000000000000000000000..0569907cd4b1576e46e874e9845573e41d6b2c47 GIT binary patch literal 264 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!3_U_u;Kfo@(Vx_&H|6f zVg?3oVGw3ym^BBaXN9MWV~EA+ry;imk0@~5Nm-%98`b76X)?pCNdBDRJdJ}r@#S@v z4X2YMx4*7RJbH8Eiwh?vUb?xpTPF7RwJl-$x97{e7Z2!^e!8Wj^vOR4*Tb)vG>xPL z87q=*O4QP@n_H&~J-e8H zx6ftPt0tkH#~018+cB}j*E87qZ)5TPiIPX}*5>TrdNbdBX@sJ`IZr;&%?zHdelF{r G5}E*;t!DuM literal 0 HcmV?d00001 diff --git a/res/icons/graphics-window/same-orientation.png b/res/icons/graphics-window/same-orientation.png new file mode 100644 index 0000000000000000000000000000000000000000..7223d0cc7326f971b86d8f83ab3ac2f0c2064ff9 GIT binary patch literal 202 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oFXyr;B5V#p&b(iHZxiy3Qmk#c9@Qs&)A|1|4?U6V&2SYRb&aJmpBb z0N+xLAQcwHmc}c pwrlZQKHqBi-R%~qeDLEI76!>BhSf$DY%4%c@^tlcS?83{1OOGBKVtv@ literal 0 HcmV?d00001 diff --git a/res/icons/graphics-window/sketch-in-3d.png b/res/icons/graphics-window/sketch-in-3d.png new file mode 100644 index 0000000000000000000000000000000000000000..d15a0d32cc0c2d7399904e8d3da5ffcd6ba630e3 GIT binary patch literal 196 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oFXmr;B5V#p&dP1wsib3iWKq3=|hk=#0`^G|RE&;Q#;sU%pzDm?OZg z*STej$(o!s3D?#{w)h^D&;IeoVe;W*0nTL)3_DDM3nX1OX-4oW9T)LwE?AkOqE^<~ j_448DO!tyNZxsdx4i)`AtC#9eK#uWr^>bP0l+XkKrgJ@J literal 0 HcmV?d00001 diff --git a/res/icons/graphics-window/sketch-in-plane.png b/res/icons/graphics-window/sketch-in-plane.png new file mode 100644 index 0000000000000000000000000000000000000000..30c99bebe65d34b52f4dcaf64e257cee5c590acc GIT binary patch literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oFX#r;B5V#p%0KPV*j6;BhWI>Tzbu!Wk>AcdD=I>Uy>~rpsS2aGpn# zW@_}qJ{OgM`_=RNnHHpb&Yk?6V-|l^%2^HGFIl-IE?*r%W^Mkz2b5OLVifs(d)sx^WKBig1ltW;K+f}Y^>bP0l+XkK DRX0hp literal 0 HcmV?d00001 diff --git a/res/icons/graphics-window/step-rotate.png b/res/icons/graphics-window/step-rotate.png new file mode 100644 index 0000000000000000000000000000000000000000..a64eae44c50606cf5e78db2df0d5516f3fb60066 GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oFYFr;B5V#p$C{4)PsP;Ba2~|Nr?n8@eNX7A{P?!=vxoDpY7{6zto$ zfZ^B5QkVF5!E=`ImEY|A>k-t{wcx5wM8cu1zoTMW54_kCCN*)zY^#R){*#w;=I>3B w7yNO`p-<nfcl|(`r;B5V#p$z?5Aq#Q;BapJ@qhoBJnIG5roAxobDPcXn(;P2@qw<= zrdw+!sWiIfwQfrNxl+zDIO<}^rU?$(EAGp7nd@4d*mvM*__Z)Slkx*hW!Jes+w!t# zty{5cn&+KRjZn4E`qjLf1E%!IPhR=yM#}9IX}4CsYOP>+w5V-iY|1)KkPAIs{an^L HB{Ts5-NsFk literal 0 HcmV?d00001 diff --git a/res/icons/graphics-window/trim.png b/res/icons/graphics-window/trim.png new file mode 100644 index 0000000000000000000000000000000000000000..de11e17aa429a0ef73fea5132b127642e7fd05ad GIT binary patch literal 224 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oFXfr;B5V#p%0~9r+p*cvvDIP5$wp`-AUmM$@(nm%<)5#wi4}ZHf+x zG83=~U+-yo{7YzA7@-;=DavMF?KS@ Np`NaOF6*2Ung9mcPcZ-h literal 0 HcmV?d00001 diff --git a/res/icons/graphics-window/vert.png b/res/icons/graphics-window/vert.png new file mode 100644 index 0000000000000000000000000000000000000000..996b191ebc904467d8effccb0b398e9b2602cd7f GIT binary patch literal 177 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oFX+r;B5V#p&dP1wsi&l*-tS87Ll9P|Mock;RtC{{R1fc>{4?*XDo5 zf7CUczDGEhF@-(naj|N$V3}Ubrjh!%@En7xhufOea~vnKmDjCMWoBU5yi11r;nIao PAe%j1{an^LB{Ts5{8KSi literal 0 HcmV?d00001 diff --git a/res/icons/text-window/constraint-dimo.png b/res/icons/text-window/constraint-dimo.png new file mode 100644 index 0000000000000000000000000000000000000000..bbe6e22c95765d27165f87a656b085df5ed4ec4b GIT binary patch literal 194 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaA@dWsUxB_X0GYtP3{sV#1{C5X| zT*i_hzhDN3XE)M-9L@rd$YKTtZeb8+WSBJvWQ@P3i(`n!#I^p8d<+U4Ouzomx1N%w zyFILMVt`8H`Uxpp4NLE?s4xYs80KnmAj|8AOVbhgJh%NB(-M3Ic>F9ncDS6>3tiJx zw3k`xNyIFLnTyYKEMKpsqJL18J7ju(?ZH#$DrcCT5_VN6G>yFNP+0P=z;E}FsMTy= zJvyrYM&VX zgWFW5O8Q911gT96>6WU|>wdT8qtUOkW*d?OryCdNo}K%D(ff;g?N{X`njxgN@xNA2{=Lc literal 0 HcmV?d00001 diff --git a/res/icons/text-window/construction.png b/res/icons/text-window/construction.png new file mode 100644 index 0000000000000000000000000000000000000000..0ecc9a130513126f01c970193aa6bc9418aa7b87 GIT binary patch literal 203 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oFY7r;B5V#p$ylw|N;9IhYT7ys>9={`q?DbLFM!Z{}yr4p^b2@}k^w zo;&-L#le!I;#x0q1?`l+9=kTjc6sL7i!I02{;aQ)m#@#;TpXDlb|k`2I$MY9ldSNq rj&8M|!3!O9!Y>{FHEUML{?uH|=mKyLDM^>bP0l+XkKX0}1t literal 0 HcmV?d00001 diff --git a/res/icons/text-window/edges.png b/res/icons/text-window/edges.png new file mode 100644 index 0000000000000000000000000000000000000000..f428de17b4d6dbc7e51dd26e5a7e83d04a118753 GIT binary patch literal 285 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oJ^lPZ!4!i_>qX9^^fuz{7HV;_pRls}`uOD&Sq$Qyt+nk(o{J`<_Zy z!C4KPXEPK>Wv?}TbI_QzA;@b#n}bVZ2b1gaYr8}~_9-o0vg-BXqg|UNpU!bTQnH+@ ze#;zV20_bE3Guok@n3RXWu_ggem_t1xoqp{Rg?8xj9V|hkFGktD8lf~gjGd*&e__p zSSTJDC=+)0+y>|9{I$26)jpm6al@hWg;ZW$@WNFtDKj6bP0l+XkKfc0pCJYnbNw6jVX0>;Y(j~zRf(RAm| z9e#PcBl!Z{df^TSdZo=fzDNnTSe0}*M*ZTLC3s|Z(}!72P16=X-nu=1|L@=LBer(^ zK0Dhy;}u60?<*zucez&{-$qaEFBQ1tS6OGzr_Bq~AE)NcGMWGiC{I^Emvv4FO#pBzfiVC8 literal 0 HcmV?d00001 diff --git a/res/icons/text-window/mesh.png b/res/icons/text-window/mesh.png new file mode 100644 index 0000000000000000000000000000000000000000..260b083f62f8bac2ad6a6523648e2dcd80bc6a88 GIT binary patch literal 335 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oJ^?PZ!4!i_=@DZWKMNz~gehQm*rsaRwKghwsZ<%m%E+0mdO*9X>5R zg>zz_rmy+WxA3roJEw=lm6{HQHQO0#K0RH!lVz0v!=A6reQJ#_Ufv086^NVmoJk?C z@Jtw!;L~eS*`E)83ahjG8eJt4vtTMi#q-VGtE;|De=NqnlmCwgtBW?P_`YqQ+1<-` zDXT}{m34l4WY)$t8|U^s&`f>HXv4X-ZAJ0*wnDARdxiYd{;rH#llJ1mMFpq(QtYly zbKiJWFDPygl23}`((Cx$&}pY8cQWv|3?Nj?cCdT&{rU)G6=j{@n lCCa}g8L*@Vg literal 0 HcmV?d00001 diff --git a/res/icons/text-window/occluded-stippled.png b/res/icons/text-window/occluded-stippled.png new file mode 100644 index 0000000000000000000000000000000000000000..04ee458eb10f83bbbca7225b621369c45694e507 GIT binary patch literal 218 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oFXxr;B5V#p$!b(R>FKIG8v6j-OdOV`l5eB7x-pQ=(Qd9LRLgK5!9o-bb4xa60^yHy&W zQ*W<5tNO!F!9HU~X5o@ki!}aA=p@eDmg1`#+`stOCAl8yjbP0 Hl+XkKPaRAB literal 0 HcmV?d00001 diff --git a/res/icons/text-window/occluded-visible.png b/res/icons/text-window/occluded-visible.png new file mode 100644 index 0000000000000000000000000000000000000000..23415c300565dc98e5e1b14c6587b8c7827edaac GIT binary patch literal 208 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oFXdr;B5V#p$H8R^#b9u x*9NVMKYcqQwr|tAX|=NNQeUwp#e47G=%>D|e`?;h`QJef^K|udS?83{1OV0BN9_Or literal 0 HcmV?d00001 diff --git a/res/icons/text-window/outlines.png b/res/icons/text-window/outlines.png new file mode 100644 index 0000000000000000000000000000000000000000..f947ec7fea5375c12135b163d20de2de9c9b83a4 GIT binary patch literal 289 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oJ^IPZ!4!i_>?n9^^b?z{BE@8xX(lVCW9D@P$$_Gv6x;h~E@6+Iju0 zy1*fUNwGY)QqS$Y$Kr6_(P7KAjuqeMZ4?j?+sxEq?4p_4EgjQ!e29sYeUZtPgk eGBY*x+*1CZw{mW@f1F_p@~Nk*pUXO@geCwc@NTmJ literal 0 HcmV?d00001 diff --git a/res/icons/text-window/point.png b/res/icons/text-window/point.png new file mode 100644 index 0000000000000000000000000000000000000000..a1a56660c3454a37e47343c6aba8a54a5f65f02b GIT binary patch literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oHuRr;B5V#p&b(3D(640*r1olhO@bd|kUFe$B{} SDPRdw&*16m=d#Wzp$Pyeh#VyV literal 0 HcmV?d00001 diff --git a/res/icons/text-window/shaded.png b/res/icons/text-window/shaded.png new file mode 100644 index 0000000000000000000000000000000000000000..0dff1ae182f995557e6eec337a170edfa6d37655 GIT binary patch literal 403 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaP3?%1DUd;wlEa{HEjtmUzPnffIy#(?Vl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=dY0)7O>#0gD)qf{>}} zW__R#Pk>K|>;M1%&CSjI{QT(Bjqr3(~iED7=pW^j0RBMrn!@^*J&=wOxg0CIvn zT^vI=t|uonG+tm3I4Sju@l5A`hG&1+1803q@XEI3yb{VGDka6VWM5m`HHMWJE?ltZ ziSl-Ke%yFvRp`S12d`vFd}VYpG%#S)+@QT9>~JhQL-Hic-zWIi-U6DgTH+c}l9E`G zYL#4+3Zxi}3=GY54NP>6j6w_ztxV0VOiZ*546FkYez3 L^>bP0l+XkKgaUcx literal 0 HcmV?d00001 diff --git a/res/icons/text-window/workplane.png b/res/icons/text-window/workplane.png new file mode 100644 index 0000000000000000000000000000000000000000..8e293cd504e1917b1e8c760a6601090956275819 GIT binary patch literal 193 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!9W@a@|Lkr1X7#@9+AZi z4BWyX%*Zfn4oFX;r;B5V#p$b4qPZ9pI9P)B{Ga-EHM`R#uB4yh*DeI)Wv>@~%N-ia ztHhxqW^>N}g=%15c(2e}savdNzZK3^9j&tV`?L6H*&OS}sbzEK>93e;{`bP2Zt*IQ hNdY&+doR?lW}ddk{o}1H*^MBVc)I$ztaD0e0sxomKEMC~ literal 0 HcmV?d00001 diff --git a/res/locales.txt b/res/locales.txt new file mode 100644 index 0000000..377d657 --- /dev/null +++ b/res/locales.txt @@ -0,0 +1,12 @@ +# This file lists the ISO locale codes (ISO 639-1/ISO 3166-1), Windows LCIDs, +# and human-readable names for every culture supported by SolveSpace. +cs-CZ,1029,Česky +de-DE,0407,Deutsch +en-US,0409,English (US) +fr-FR,040C,Français +es-AR,2C0A,español (AR) +ru-RU,0419,Русский +tr-TR,041F,Türkçe +uk-UA,0422,Українська +zh-CN,0804,简体中文 +ja-JP,0411,日本語 diff --git a/res/locales/cs_CZ.po b/res/locales/cs_CZ.po new file mode 100644 index 0000000..b0a3e3d --- /dev/null +++ b/res/locales/cs_CZ.po @@ -0,0 +1,2300 @@ +# Czech translations for SolveSpace package. +# Copyright (C) 2022 the SolveSpace authors +# This file is distributed under the same license as the SolveSpace package. +# Pavel Stržínek , 2022 +# +msgid "" +msgstr "" +"Project-Id-Version: SolveSpace 3.1\n" +"Report-Msgid-Bugs-To: phkahler@gmail.com\n" +"POT-Creation-Date: 2025-01-26 21:04+0200\n" +"PO-Revision-Date: 2022-06-19 20:16+0200\n" +"Last-Translator: Pavel Stržínek \n" +"Language-Team: none\n" +"Language: cs_CZ\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: clipboard.cpp:314 +msgid "" +"Cut, paste, and copy work only in a workplane.\n" +"\n" +"Activate one with Sketch -> In Workplane." +msgstr "" +"Kopírování, vkládání a vyjímání je funkční jen v pracovní rovině.\n" +"\n" +"Aktivuj ji přes Náčrt -> V pracovní rovině." + +#: clipboard.cpp:331 +msgid "Clipboard is empty; nothing to paste." +msgstr "Schránka je prázdná; nic ke vložení." + +#: clipboard.cpp:378 +msgid "Number of copies to paste must be at least one." +msgstr "Je třeba zadat alespoň jednu kopii ke vložení." + +#: clipboard.cpp:394 textscreens.cpp:879 +msgid "Scale cannot be zero." +msgstr "Měřítko nemůže být nulové." + +#: clipboard.cpp:436 +msgid "Select one point to define origin of rotation." +msgstr "Vyber jeden bod pro určení počátku otočení." + +#: clipboard.cpp:448 +msgid "Select two points to define translation vector." +msgstr "Vyber dva body pro určení vektoru posunutí." + +#: clipboard.cpp:458 +msgid "" +"Transformation is identity. So all copies will be exactly on top of each " +"other." +msgstr "Transformace je identická. VÅ¡echny kopie budou přesně nad sebou." + +#: clipboard.cpp:462 +msgid "Too many items to paste; split this into smaller pastes." +msgstr "PříliÅ¡ mnoho položek ke vložení; rozděl je a vlož po částech." + +#: clipboard.cpp:467 +msgid "No workplane active." +msgstr "Není aktivní žádná pracovní rovina." + +#: confscreen.cpp:410 +msgid "Bad format: specify coordinates as x, y, z" +msgstr "Chybný formát: zadej souřadnice jako x, y, z" + +#: confscreen.cpp:420 style.cpp:729 textscreens.cpp:910 +msgid "Bad format: specify color as r, g, b" +msgstr "Chybný formát: zadej barvu jako r, g, b" + +#: confscreen.cpp:446 +msgid "" +"The perspective factor will have no effect until you enable View -> Use " +"Perspective Projection." +msgstr "" +"Faktor perspektivy nebude mít žádný vliv, dokud nezvolíš Pohled -> Použít " +"perspektivní projekci." + +#: confscreen.cpp:464 confscreen.cpp:474 +#, c-format +msgid "Specify between 0 and %d digits after the decimal." +msgstr "Urči počet platných číslic za desetinnou čárkou mezi 0 a %d." + +#: confscreen.cpp:486 +msgid "Export scale must not be zero!" +msgstr "Měřítko exportu nesmí být nulové!" + +#: confscreen.cpp:498 +msgid "Cutter radius offset must not be negative!" +msgstr "Posunutí poloměru výřezu nesmí být záporné!" + +#: confscreen.cpp:557 +msgid "Bad value: autosave interval should be positive" +msgstr "Chybná hodnota: interval automatického ukládání by měl být kladný" + +#: confscreen.cpp:560 +msgid "Bad format: specify interval in integral minutes" +msgstr "Chybný formát: zadej interval v celých minutách" + +#: constraint.cpp:12 +msgctxt "constr-name" +msgid "pts-coincident" +msgstr "shoda-bodů" + +#: constraint.cpp:13 +msgctxt "constr-name" +msgid "pt-pt-distance" +msgstr "vzdálenost-dvou-bodů" + +#: constraint.cpp:14 +msgctxt "constr-name" +msgid "pt-line-distance" +msgstr "vzdálenost-bodu-a-úsečky" + +#: constraint.cpp:15 +msgctxt "constr-name" +msgid "pt-plane-distance" +msgstr "vzdálenost-bodu-a-roviny" + +#: constraint.cpp:16 +msgctxt "constr-name" +msgid "pt-face-distance" +msgstr "vzdálenost-bodu-a-stěny" + +#: constraint.cpp:17 +msgctxt "constr-name" +msgid "proj-pt-pt-distance" +msgstr "proj-vzdálenost-dvou-bodů" + +#: constraint.cpp:18 +msgctxt "constr-name" +msgid "pt-in-plane" +msgstr "bod-v-rovině" + +#: constraint.cpp:19 +msgctxt "constr-name" +msgid "pt-on-line" +msgstr "bod-na-přímce" + +#: constraint.cpp:20 +msgctxt "constr-name" +msgid "pt-on-face" +msgstr "bod-na-stěně" + +#: constraint.cpp:21 +msgctxt "constr-name" +msgid "eq-length" +msgstr "shodná-délka" + +#: constraint.cpp:22 +msgctxt "constr-name" +msgid "eq-length-and-pt-ln-dist" +msgstr "shodná-délka-a-vzdálenost-bodu-a-úsečky" + +#: constraint.cpp:23 +msgctxt "constr-name" +msgid "eq-pt-line-distances" +msgstr "shodná-vzdálenost-bodu-a-úsečky" + +#: constraint.cpp:24 +msgctxt "constr-name" +msgid "length-ratio" +msgstr "poměr-délky" + +#: constraint.cpp:25 +msgctxt "constr-name" +msgid "arc-arc-length-ratio" +msgstr "poměr-délky-dvou-oblouků" + +#: constraint.cpp:26 +msgctxt "constr-name" +msgid "arc-line-length-ratio" +msgstr "poměr-délky-oblouku-a-úsečky" + +#: constraint.cpp:27 +msgctxt "constr-name" +msgid "length-difference" +msgstr "rozdíl-délky" + +#: constraint.cpp:28 +msgctxt "constr-name" +msgid "arc-arc-len-difference" +msgstr "rozdíl-délky-dvou-oblouků" + +#: constraint.cpp:29 +msgctxt "constr-name" +msgid "arc-line-len-difference" +msgstr "rozdíl-délky-oblouku-a-úsečky" + +#: constraint.cpp:30 +msgctxt "constr-name" +msgid "symmetric" +msgstr "symetrie" + +#: constraint.cpp:31 +msgctxt "constr-name" +msgid "symmetric-h" +msgstr "symetrie-h" + +#: constraint.cpp:32 +msgctxt "constr-name" +msgid "symmetric-v" +msgstr "symetrie-v" + +#: constraint.cpp:33 +msgctxt "constr-name" +msgid "symmetric-line" +msgstr "symetrie-přímka" + +#: constraint.cpp:34 +msgctxt "constr-name" +msgid "at-midpoint" +msgstr "ve-středu" + +#: constraint.cpp:35 +msgctxt "constr-name" +msgid "horizontal" +msgstr "horizontála" + +#: constraint.cpp:36 +msgctxt "constr-name" +msgid "vertical" +msgstr "vertikála" + +#: constraint.cpp:37 +msgctxt "constr-name" +msgid "diameter" +msgstr "průměr" + +#: constraint.cpp:38 +msgctxt "constr-name" +msgid "pt-on-circle" +msgstr "bod-na-kruhu" + +#: constraint.cpp:39 +msgctxt "constr-name" +msgid "same-orientation" +msgstr "shodná-orientace" + +#: constraint.cpp:40 +msgctxt "constr-name" +msgid "angle" +msgstr "úhel" + +#: constraint.cpp:41 +msgctxt "constr-name" +msgid "parallel" +msgstr "rovnoběžnost" + +#: constraint.cpp:42 +msgctxt "constr-name" +msgid "arc-line-tangent" +msgstr "tečný-oblouk-a-úsečka" + +#: constraint.cpp:43 +msgctxt "constr-name" +msgid "cubic-line-tangent" +msgstr "tečná-splajna-a-úsečka" + +#: constraint.cpp:44 +msgctxt "constr-name" +msgid "curve-curve-tangent" +msgstr "tečné-dvě-křivky" + +#: constraint.cpp:45 +msgctxt "constr-name" +msgid "perpendicular" +msgstr "kolmost" + +#: constraint.cpp:46 +msgctxt "constr-name" +msgid "eq-radius" +msgstr "shodný-poloměr" + +#: constraint.cpp:47 +msgctxt "constr-name" +msgid "eq-angle" +msgstr "shodný-úhel" + +#: constraint.cpp:48 +msgctxt "constr-name" +msgid "eq-line-len-arc-len" +msgstr "shodná-délka-úsečky-a-oblouku" + +#: constraint.cpp:49 +msgctxt "constr-name" +msgid "lock-where-dragged" +msgstr "zámek-kam-přetažen" + +#: constraint.cpp:50 +msgctxt "constr-name" +msgid "comment" +msgstr "komentář" + +#: constraint.cpp:151 +msgid "" +"The point you selected does not belong to the arc. The arc and line segment " +"do not share an end point.\n" +"\n" +"Select the end point of the arc at which you want it to be tangent to the " +"line." +msgstr "" +"Vámi vybraný bod nepatří do oblouku. Oblouk a úsečka " +"nemají společný koncový bod.\n" +"\n" +"Vyberte koncový bod oblouku, ve kterém má být tečný k " +"přímce." + +#: constraint.cpp:158 +msgid "" +"The tangent arc and line segment must share an endpoint. Constrain them with " +"Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the arc at which you want it to be " +"tangent to the line." +msgstr "" +"Oblouk a tečná úsečka musejí mít společný koncový bod. Omez je nejdříve " +"příkazem Omezit -> Na bodě / křivce / ploÅ¡e.\n" +"\n" +"Případně vyberte koncový bod oblouku, ve kterém má být " +"tečný k přímce." + +#: constraint.cpp:186 +msgid "" +"The point you selected is not an end point of the cubic spline. The spline " +"and line segment do not share an end point.\n" +"\n" +"Select the end point of the spline at which you want it to be tangent to the " +"line." +msgstr "" +"Vybraný bod není koncovým bodem splajnu. Splajn " +"a úsečka nemají společný koncový bod.\n" +"\n" +"Vyberte koncový bod splajnu, ve kterém má být tečný k " +"přímce." + +#: constraint.cpp:193 +msgid "" +"The tangent cubic spline and line segment must share an endpoint. Constrain " +"them with Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the cubic spline at which you want it " +"to be tangent to the line." +msgstr "" +"Tečný splajn a úsečka musejí mít společný koncový bod. Omez je nejdříve " +"příkazem Omezit -> Na bodě / křivce / ploÅ¡e.\n" +"\n" +"Případně vyberte koncový bod splajnu, ve kterém má být tečný k " +"přímce." + +#: constraint.cpp:237 +msgid "" +"The points you selected are not end points of the two curves. The curves do " +"not share an end point.\n" +"\n" +"Select the end points of both curves at which you want them to be tangent to " +"each other." +msgstr "" +"Vybrané body nejsou koncovými body dvou křivek. Křivky " +"nemají společný koncový bod.\n" +"\n" +"Vyberte koncové body obou křivek, v nichž mají být vůči sobě " +"tečné" + +#: constraint.cpp:244 +msgid "" +"The curves must share an endpoint. Constrain them with Constrain -> On Point " +"before constraining tangent.\n" +"\n" +"Alternatively select the end points of both curves at which you want the " +"curves to be tangent." +msgstr "" +"Křivky musejí mít společný koncový bod. Omez je příkazem Omezit -> V bodě " +"před omezením tečny.\n" +"\n" +"Případně vyberte koncové body obou křivek, v nichž mají být vůči sobě " +"tečné" + +#: constraint.cpp:303 +msgid "" +"Bad selection for distance / diameter constraint. This constraint can apply " +"to:\n" +"\n" +" * two points (distance between points)\n" +" * a line segment (length)\n" +" * two points and a line segment or normal (projected distance)\n" +" * a workplane and a point (minimum distance)\n" +" * a line segment and a point (minimum distance)\n" +" * a plane face and a point (minimum distance)\n" +" * a circle or an arc (diameter)\n" +msgstr "" +"Chybný výběr pro omezení vzdálenosti / průměru. Toto omezení lze použít " +"pro:\n" +"\n" +" * dva body (vzdálenost mezi body)\n" +" * úsečku (délka)\n" +" * dva body a úsečku nebo normálu (promítnutá vzdálenost)\n" +" * pracovní rovinu a bod (minimální vzdálenost)\n" +" * úsečku a bod (minimální vzdálenost)\n" +" * rovinnou plochu a bod (minimální vzdálenost)\n" +" * kružnici nebo oblouk (průměr)\n" + +#: constraint.cpp:366 +msgid "" +"Bad selection for on point / curve / plane constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points (points coincident)\n" +" * a point and a workplane (point in plane)\n" +" * a point and a line segment (point on line)\n" +" * a point and a circle or arc (point on curve)\n" +" * a point and one to three plane faces (point on face(s))\n" +msgstr "" +"Chybný výběr pro omezení na bod / křivku / rovinu. Toto omezení lze použít " +"pro:\n" +"\n" +" * dva a více bodů (body se shodují)\n" +" * bod a pracovní rovinu (bod v rovině)\n" +" * bod a úsečku (bod na přímce)\n" +" * bod a kružnici nebo oblouk (bod na křivce)\n" +" * bod a jednu až tři rovinné plochy (bod na ploÅ¡e/plochách)\n" + +#: constraint.cpp:427 +msgid "" +"Bad selection for equal length / radius constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more line segments (equal length)\n" +" * two line segments and two points (equal point-line distances)\n" +" * a line segment and two points (equal point-line distances)\n" +" * a line segment, and a point and line segment (point-line distance " +"equals length)\n" +" * two or more circles or arcs (equal radius)\n" +" * a line segment and an arc (line segment length equals arc length)\n" +msgstr "" +"Chybný výběr pro omezení stejné délky / poloměru. Toto omezení lze použít " +"pro:\n" +"\n" +" * dvě a více úseček (stejné délky)\n" +" * dvě úsečky a dva body (stejné vzdálenosti bodů od úseček)\n" +" * úsečku a dva body (stejné vzdálenosti bodů od přímky)\n" +" * úsečku a bod a úsečku (vzdálenost bodu od přímky se rovná délce)\n" +" * dvě a více kružnic nebo oblouků (stejný poloměr)\n" +" * úsečku a oblouk (délka úsečky se rovná délce oblouku)\n" + +#: constraint.cpp:480 +msgid "" +"Bad selection for length ratio constraint. This constraint can apply to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"Chybný výběr pro omezení poměru délky. Toto omezení lze použít pro:\n" +"\n" +" * dvě úsečky\n" +" * dva oblouky\n" +" * jeden oblouk a jednu úsečku\n" + +#: constraint.cpp:515 +msgid "" +"Bad selection for length difference constraint. This constraint can apply " +"to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"Chybný výběr pro omezení rozdílu délek. Toto omezení lze použít pro:\n" +"\n" +" * dvě úsečky\n" +" * dva oblouky\n" +" * jeden oblouk a jednu úsečku\n" + +#: constraint.cpp:550 +msgid "" +"Bad selection for at midpoint constraint. This constraint can apply to:\n" +"\n" +" * a line segment and a point (point at midpoint)\n" +" * a line segment and a workplane (line's midpoint on plane)\n" +msgstr "" +"Chybný výběr pro omezení ve středovém bodě. Toto omezení lze použít pro: \n" +"\n" +" * úsečku a bod (bod ve středovém bodě)\n" +" * úsečku a pracovní rovinu (střed úsečky v rovině)\n" + +#: constraint.cpp:608 +msgid "" +"Bad selection for symmetric constraint. This constraint can apply to:\n" +"\n" +" * two points or a line segment (symmetric about workplane's coordinate " +"axis)\n" +" * line segment, and two points or a line segment (symmetric about line " +"segment)\n" +" * workplane, and two points or a line segment (symmetric about " +"workplane)\n" +msgstr "" +"Chybný výběr pro symetrické omezení. Toto omezení lze použít pro:\n" +"\n" +" * dva body nebo úsečku (symetrickou vůči souřadnicím osy pracovní " +"roviny)\n" +" * úsečku a dva body nebo úsečku (symetrická k úsečce)\n" +" * pracovní rovinu a dva body nebo úsečku (symetrická k pracovní rovině)\n" + +#: constraint.cpp:623 +msgid "" +"A workplane must be active when constraining symmetric without an explicit " +"symmetry plane." +msgstr "" +"Pracovní rovina musí být aktivní, pokud je omezena symetricky bez explicitní " +"symetrické roviny." + +#: constraint.cpp:663 +msgid "" +"Activate a workplane (with Sketch -> In Workplane) before applying a " +"horizontal or vertical constraint." +msgstr "" +"Aktivuj pracovní rovinu (pomocí Náčrt -> V pracovní rovině) před použitím " +"omezení horizontály nebo vertikály." + +#: constraint.cpp:679 +msgid "" +"Bad selection for horizontal / vertical constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points\n" +" * one or more line segments\n" +msgstr "" +"Chybný výběr pro horizontální / vertikální omezení. Toto omezení lze použít " +"pro:\n" +"\n" +" * dva a více bodů\n" +" * jednu a více úseček\n" + +#: constraint.cpp:697 +msgid "" +"Bad selection for same orientation constraint. This constraint can apply " +"to:\n" +"\n" +" * two normals\n" +msgstr "" +"Chybný výběr pro omezení stejné orientace. Toto omezení lze použít pro: \n" +" \n" +" * dvě normály\n" + +#: constraint.cpp:748 +msgid "Must select an angle constraint." +msgstr "Je nutné vybrat omezení úhlu." + +#: constraint.cpp:761 +msgid "Must select a constraint with associated label." +msgstr "Je nutné vybrat omezení s přiřazeným Å¡títkem." + +#: constraint.cpp:784 +msgid "" +"Bad selection for angle constraint. This constraint can apply to:\n" +"\n" +"Angle between:\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +"\n" +"Equal angles:\n" +" * four line segments or normals (equal angle between A,B and C,D)\n" +" * three line segments or normals (equal angle between A,B and B,C)\n" +msgstr "" +"Chybný výběr pro omezení úhlu. Toto omezení lze použít pro: \n" +"\n" +"Úhel mezi:\n" +" * dvěma úsečkami\n" +" * úsečkou a normálou\n" +" * dvěma normálami\n" +"\n" +"Shodné úhly:\n" +" * čtyři úsečky nebo normály (shodný úhel mezi A,B a C,D)\n" +" * tři úsečky nebo normály (shodný úhel mezi A,B a B,C)\n" + +#: constraint.cpp:872 +msgid "Curve-curve tangency must apply in workplane." +msgstr "Tečnost dvou křivek musí být použita v pracovní rovině." + +#: constraint.cpp:887 +msgid "" +"Bad selection for parallel / tangent constraint. This constraint can apply " +"to:\n" +"\n" +" * two faces\n" +" * two or more line segments (parallel)\n" +" * one or more line segments and one or more normals (parallel)\n" +" * two or more normals (parallel)\n" +" * two line segments, arcs, or beziers, that share an endpoint (tangent)\n" +" * two line segments, arcs, or beziers, that do not share an endpoint and " +"the end point(s) of the curve(s) (tangent)\n" +msgstr "" +"Chybný výběr pro rovnoběžné / tečné omezení. Toto omezení lze použít pro:\n" +" \n" +" * dvě plochy\n" +" * dvě a více úseček (rovnoběžné)\n" +" * jednu a více úseček a jednu a více normál (rovnoběžné)\n" +" * dvě a více normál (rovnoběžné)\n" +" * dvě úsečky, oblouky nebo beziéry, které mají společný koncový bod " +"(tečné)\n" +" * dvě úsečky, oblouky nebo beziéry, které nemají společný koncový bod, a " +"koncový(é) bod(y) křivky(ek) (tečné)\n" + +#: constraint.cpp:914 +msgid "" +"Bad selection for perpendicular constraint. This constraint can apply to:\n" +"\n" +" * two faces\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +msgstr "" +"Chybný výběr pro omezení kolmosti. Toto omezení lze použít pro: \n" +"\n" +" * dvě plochy\n" +" * dvě úsečky\n" +" * úsečku a normálu\n" +" * dvě normály\n" + +#: constraint.cpp:931 +msgid "" +"Bad selection for lock point where dragged constraint. This constraint can " +"apply to:\n" +"\n" +" * a point\n" +msgstr "" +"Chybný výběr pro omezení uzamčení v místě přetažení. Toto omezení lze použít " +"pro: \n" +"\n" +" * bod\n" + +#: constraint.cpp:946 mouse.cpp:1160 +msgid "NEW COMMENT -- DOUBLE-CLICK TO EDIT" +msgstr "NOVÝ KOMENTÁŘ -- UPRAVIT DVOJKLIKEM" + +#: constraint.cpp:952 +msgid "click center of comment text" +msgstr "klikni na střed textu komentáře" + +#: export.cpp:19 +msgid "" +"No solid model present; draw one with extrudes and revolves, or use Export " +"2d View to export bare lines and curves." +msgstr "" +"Není přítomen žádný model tělesa; vytvoř model pomocí extruzí a rotací nebo " +"použij Exportovat 2D pohled... pro export prostých čar a křivek." + +#: export.cpp:61 +msgid "" +"Bad selection for export section. Please select:\n" +"\n" +" * nothing, with an active workplane (workplane is section plane)\n" +" * a face (section plane through face)\n" +" * a point and two line segments (plane through point and parallel to " +"lines)\n" +msgstr "" +"Chybný výběr části pro export. Vyber prosím:\n" +"\n" +" * nic, s aktivní pracovní rovinou (pracovní rovina je rovina řezu)\n" +" * plochu (rovina řezu procházející plochou)\n" +" * bod a dvě úsečky (rovina procházející bodem a rovnoběžná s přímkami)\n" + +#: export.cpp:818 +msgid "Active group mesh is empty; nothing to export." +msgstr "Síť aktivní skupiny je prázdná; není co exportovat." + +#: exportvector.cpp:336 +msgid "freehand lines were replaced with continuous lines" +msgstr "volné úsečky byly nahrazeny souvislými" + +#: exportvector.cpp:338 +msgid "zigzag lines were replaced with continuous lines" +msgstr "klikaté úsečky byly nahrazeny souvislými" + +#: exportvector.cpp:592 +msgid "" +"Some aspects of the drawing have no DXF equivalent and were not exported:\n" +msgstr "" +"Některé prvky výkresu nemají ekvivalent ve formátu DXF a nebyly " +"exportovány:\n" + +#: exportvector.cpp:838 +msgid "" +"PDF page size exceeds 200 by 200 inches; many viewers may reject this file." +msgstr "" +"Velikost stránky PDF přesahuje 200 x 200 palců; mnoho prohlížečů může tento " +"soubor odmítnout." + +#: file.cpp:44 group.cpp:91 +msgctxt "group-name" +msgid "sketch-in-plane" +msgstr "náčrt-v-rovině" + +#: file.cpp:62 +msgctxt "group-name" +msgid "#references" +msgstr "#odkazy" + +#: file.cpp:555 +msgid "The file is empty. It may be corrupt." +msgstr "Soubor je prázdný. Může být poÅ¡kozený." + +#: file.cpp:560 +msgid "" +"Unrecognized data in file. This file may be corrupt, or from a newer version " +"of the program." +msgstr "" +"Nerozpoznaná data v souboru. Tento soubor může být poÅ¡kozený nebo pochází z " +"novější verze programu." + +#: file.cpp:876 +msgctxt "title" +msgid "Missing File" +msgstr "Chybějící soubor" + +#: file.cpp:877 +#, c-format +msgctxt "dialog" +msgid "The linked file “%s” is not present." +msgstr "Odkazovaný soubor “%s” není k dispozici." + +#: file.cpp:879 +msgctxt "dialog" +msgid "" +"Do you want to locate it manually?\n" +"\n" +"If you decline, any geometry that depends on the missing file will be " +"permanently removed." +msgstr "" +"ChceÅ¡ jej vyhledat ručně?\n" +"\n" +"Pokud odmítneÅ¡, vÅ¡echny geometrie, které závisí na chybějícím souboru, budou " +"trvale odstraněny." + +#: file.cpp:882 +msgctxt "button" +msgid "&Yes" +msgstr "&Ano" + +#: file.cpp:884 +msgctxt "button" +msgid "&No" +msgstr "&Ne" + +#: file.cpp:886 solvespace.cpp:652 +msgctxt "button" +msgid "&Cancel" +msgstr "&ZruÅ¡it" + +#: graphicswin.cpp:41 +msgid "&File" +msgstr "&Soubor" + +#: graphicswin.cpp:42 +msgid "&New" +msgstr "&Nový" + +#: graphicswin.cpp:43 +msgid "&Open..." +msgstr "&Otevřít..." + +#: graphicswin.cpp:44 +msgid "Open &Recent" +msgstr "Otevřít &nedávný" + +#: graphicswin.cpp:45 +msgid "&Save" +msgstr "&Uložit" + +#: graphicswin.cpp:46 +msgid "Save &As..." +msgstr "Uložit &jako..." + +#: graphicswin.cpp:48 +msgid "Export &Image..." +msgstr "Exportovat &Obrázek..." + +#: graphicswin.cpp:49 +msgid "Export 2d &View..." +msgstr "Exportovat 2D &pohled..." + +#: graphicswin.cpp:50 +msgid "Export 2d &Section..." +msgstr "Exportovat 2D čá&st..." + +#: graphicswin.cpp:51 +msgid "Export 3d &Wireframe..." +msgstr "Exportovat 3D &drátěný model..." + +#: graphicswin.cpp:52 +msgid "Export Triangle &Mesh..." +msgstr "Exportovat &trojúhelníkovou síť (mesh)..." + +#: graphicswin.cpp:53 +msgid "Export &Surfaces..." +msgstr "Exportovat &povrchy..." + +#: graphicswin.cpp:54 +msgid "Im&port..." +msgstr "&Importovat..." + +#: graphicswin.cpp:57 +msgid "E&xit" +msgstr "U&končit" + +#: graphicswin.cpp:60 +msgid "&Edit" +msgstr "Up&ravit" + +#: graphicswin.cpp:61 +msgid "&Undo" +msgstr "&Zpět" + +#: graphicswin.cpp:62 +msgid "&Redo" +msgstr "Z&novu" + +#: graphicswin.cpp:63 +msgid "Re&generate All" +msgstr "Znovu vÅ¡e &generovat" + +#: graphicswin.cpp:65 +msgid "Snap Selection to &Grid" +msgstr "Přichytit výběr k &mřížce" + +#: graphicswin.cpp:66 +msgid "Rotate Imported &90°" +msgstr "Otočit importované o &90°" + +#: graphicswin.cpp:68 +msgid "Cu&t" +msgstr "Vyjmou&t" + +#: graphicswin.cpp:69 +msgid "&Copy" +msgstr "&Kopírovat" + +#: graphicswin.cpp:70 +msgid "&Paste" +msgstr "V&ložit" + +#: graphicswin.cpp:71 +msgid "Paste &Transformed..." +msgstr "Vložit &transformované..." + +#: graphicswin.cpp:72 +msgid "&Delete" +msgstr "O&dstranit" + +#: graphicswin.cpp:74 +msgid "Select &Edge Chain" +msgstr "Vybrat ř&etězec hran" + +#: graphicswin.cpp:75 +msgid "Select &All" +msgstr "Vybr&at vÅ¡e" + +#: graphicswin.cpp:76 +msgid "&Unselect All" +msgstr "Zr&uÅ¡it výběr vÅ¡eho" + +#: graphicswin.cpp:78 +msgid "&Line Styles..." +msgstr "Sty&ly čar..." + +#: graphicswin.cpp:79 +msgid "&View Projection..." +msgstr "&Projekční pohled..." + +#: graphicswin.cpp:81 +msgid "Con&figuration..." +msgstr "Nastave&ní..." + +#: graphicswin.cpp:84 +msgid "&View" +msgstr "&Zobrazení" + +#: graphicswin.cpp:85 +msgid "Zoom &In" +msgstr "Přiblíž&it" + +#: graphicswin.cpp:86 +msgid "Zoom &Out" +msgstr "&Oddálit" + +#: graphicswin.cpp:87 +msgid "Zoom To &Fit" +msgstr "Přiblížit na &míru" + +#: graphicswin.cpp:89 +msgid "Align View to &Workplane" +msgstr "Zarovnat pohled na pracovní rovinu" + +#: graphicswin.cpp:90 +msgid "Nearest &Ortho View" +msgstr "Nejbližší &ortogonální pohled" + +#: graphicswin.cpp:91 +msgid "Nearest &Isometric View" +msgstr "Nejbližší &izometrický pohled" + +#: graphicswin.cpp:92 +msgid "&Center View At Point" +msgstr "&Pohled na střed v bodě" + +#: graphicswin.cpp:94 +msgid "Show Snap &Grid" +msgstr "Zobrazit mřížku přichy&cení" + +#: graphicswin.cpp:95 +msgid "Darken Inactive Solids" +msgstr "Ztmavit neaktivní tělesa" + +#: graphicswin.cpp:96 +msgid "Use &Perspective Projection" +msgstr "Použít &perspektivní projekci" + +#: graphicswin.cpp:97 +msgid "Show E&xploded View" +msgstr "Zobrazit &rozbalený pohled" + +#: graphicswin.cpp:98 +msgid "Dimension &Units" +msgstr "&Jednotky rozměru" + +#: graphicswin.cpp:99 +msgid "Dimensions in &Millimeters" +msgstr "Rozměry v &milimetrech" + +#: graphicswin.cpp:100 +msgid "Dimensions in M&eters" +msgstr "Rozměry v m&etrech" + +#: graphicswin.cpp:101 +msgid "Dimensions in &Inches" +msgstr "Rozměry v &palcích" + +#: graphicswin.cpp:102 +msgid "Dimensions in &Feet and Inches" +msgstr "Rozměry ve &stopách a palcích" + +#: graphicswin.cpp:104 +msgid "Show &Toolbar" +msgstr "Zobrazit panel &nástrojů" + +#: graphicswin.cpp:105 +msgid "Show Property Bro&wser" +msgstr "Zobrazit ¶metry" + +#: graphicswin.cpp:107 +msgid "&Full Screen" +msgstr "Na &celou obrazovku" + +#: graphicswin.cpp:109 +msgid "&New Group" +msgstr "&Nová skupina" + +#: graphicswin.cpp:110 +msgid "Sketch In &3d" +msgstr "Náčrt ve &3D" + +#: graphicswin.cpp:111 +msgid "Sketch In New &Workplane" +msgstr "Náčrt v nové pracovní &rovině" + +#: graphicswin.cpp:113 +msgid "Step &Translating" +msgstr "Krokový &posun" + +#: graphicswin.cpp:114 +msgid "Step &Rotating" +msgstr "Krokové &otočení" + +#: graphicswin.cpp:116 +msgid "E&xtrude" +msgstr "Extruze (e&xtrude)" + +#: graphicswin.cpp:117 +msgid "&Helix" +msgstr "Å roubovice (&helix)" + +#: graphicswin.cpp:118 +msgid "&Lathe" +msgstr "Plná rotace (&lathe)" + +#: graphicswin.cpp:119 +msgid "Re&volve" +msgstr "Volná rotace (re&volve)" + +#: graphicswin.cpp:121 +msgid "Link / Assemble..." +msgstr "Odkaz / Sestavení..." + +#: graphicswin.cpp:122 +msgid "Link Recent" +msgstr "Odkaz na nedávný" + +#: graphicswin.cpp:124 +msgid "&Sketch" +msgstr "&Náčrt" + +#: graphicswin.cpp:125 +msgid "In &Workplane" +msgstr "V &pracovní rovině" + +#: graphicswin.cpp:126 +msgid "Anywhere In &3d" +msgstr "Kdekoliv ve &3D" + +#: graphicswin.cpp:128 +msgid "Datum &Point" +msgstr "Vztažný &Bod" + +#: graphicswin.cpp:129 +msgid "Wor&kplane" +msgstr "&Pracovní rovina" + +#: graphicswin.cpp:131 +msgid "Line &Segment" +msgstr "Úsečka" + +#: graphicswin.cpp:132 +msgid "C&onstruction Line Segment" +msgstr "Konstrukční úsečka" + +#: graphicswin.cpp:133 +msgid "&Rectangle" +msgstr "&Obdélník" + +#: graphicswin.cpp:134 +msgid "&Circle" +msgstr "&Kružnice" + +#: graphicswin.cpp:135 +msgid "&Arc of a Circle" +msgstr "&Oblouk kružnice" + +#: graphicswin.cpp:136 +msgid "&Bezier Cubic Spline" +msgstr "&Bézierův kubický splajn" + +#: graphicswin.cpp:138 +msgid "&Text in TrueType Font" +msgstr "&Text v písmu TrueType" + +#: graphicswin.cpp:139 +msgid "I&mage" +msgstr "&Obrázek" + +#: graphicswin.cpp:141 +msgid "To&ggle Construction" +msgstr "Přepnout &konstrukci" + +#: graphicswin.cpp:142 +msgid "Ta&ngent Arc at Point" +msgstr "Tečný &oblouk v bodě" + +#: graphicswin.cpp:143 +msgid "Split Curves at &Intersection" +msgstr "Rozdělit kř&ivky v průsečíku" + +#: graphicswin.cpp:145 +msgid "&Constrain" +msgstr "Ome&zení" + +#: graphicswin.cpp:146 +msgid "&Distance / Diameter" +msgstr "&Vzdálenost / průměr" + +#: graphicswin.cpp:147 +msgid "Re&ference Dimension" +msgstr "Re&ferenční rozměr" + +#: graphicswin.cpp:148 +msgid "A&ngle / Equal Angle" +msgstr "Úhe&l / shodný úhel" + +#: graphicswin.cpp:149 +msgid "Reference An&gle" +msgstr "Refe&renční úhel" + +#: graphicswin.cpp:150 +msgid "Other S&upplementary Angle" +msgstr "Další doplňkový úh&el" + +#: graphicswin.cpp:151 +msgid "Toggle R&eference Dim" +msgstr "Přepnout r&eferenční rozměr" + +#: graphicswin.cpp:153 +msgid "&Horizontal" +msgstr "&Horizontála" + +#: graphicswin.cpp:154 +msgid "&Vertical" +msgstr "&Vertikála" + +#: graphicswin.cpp:156 +msgid "&On Point / Curve / Plane" +msgstr "&Na bodě / křivce / ploÅ¡e" + +#: graphicswin.cpp:157 +msgid "E&qual Length / Radius" +msgstr "Shodná dél&ka / poloměr" + +#: graphicswin.cpp:158 +msgid "Length / Arc Ra&tio" +msgstr "Poměr délky / oblo&uku" + +#: graphicswin.cpp:159 +msgid "Length / Arc Diff&erence" +msgstr "Rozdíl &délky / oblouku" + +#: graphicswin.cpp:160 +msgid "At &Midpoint" +msgstr "Ve středové&m bodě" + +#: graphicswin.cpp:161 +msgid "S&ymmetric" +msgstr "S&ymetrie" + +#: graphicswin.cpp:162 +msgid "Para&llel / Tangent" +msgstr "Rovno&běžnost / tečna" + +#: graphicswin.cpp:163 +msgid "&Perpendicular" +msgstr "&Kolmost" + +#: graphicswin.cpp:164 +msgid "Same Orient&ation" +msgstr "Shodná orient&ace" + +#: graphicswin.cpp:165 +msgid "Lock Point Where &Dragged" +msgstr "Uzamčení v místě pře&tažení" + +#: graphicswin.cpp:167 +msgid "Comment" +msgstr "Komentář" + +#: graphicswin.cpp:169 +msgid "&Analyze" +msgstr "&Analyzovat" + +#: graphicswin.cpp:170 +msgid "Measure &Volume" +msgstr "Měření &objemu" + +#: graphicswin.cpp:171 +msgid "Measure A&rea" +msgstr "Měření &plochy" + +#: graphicswin.cpp:172 +msgid "Measure &Perimeter" +msgstr "Měření ob&vodu" + +#: graphicswin.cpp:173 +msgid "Show &Interfering Parts" +msgstr "Zobrazit kol&idující části" + +#: graphicswin.cpp:174 +msgid "Show &Naked Edges" +msgstr "Zobrazit ob&nažené hrany" + +#: graphicswin.cpp:175 +msgid "Show &Center of Mass" +msgstr "Zobrazit &těžiÅ¡tě" + +#: graphicswin.cpp:177 +msgid "Show &Underconstrained Points" +msgstr "Zobrazit &nedostatečně omezené body" + +#: graphicswin.cpp:179 +msgid "&Trace Point" +msgstr "&Trasovat bod" + +#: graphicswin.cpp:180 +msgid "&Stop Tracing..." +msgstr "&Zastavit trasování..." + +#: graphicswin.cpp:181 +msgid "Step &Dimension..." +msgstr "&Rozměr kroku..." + +#: graphicswin.cpp:183 +msgid "&Help" +msgstr "&Nápověda" + +#: graphicswin.cpp:184 +msgid "&Language" +msgstr "&Jazyk" + +#: graphicswin.cpp:185 +msgid "&Website / Manual" +msgstr "&Web / Manuál" + +#: graphicswin.cpp:186 +msgid "&Go to GitHub commit" +msgstr "Revize na &GitHubu" + +#: graphicswin.cpp:188 +msgid "&About" +msgstr "O &aplikaci" + +#: graphicswin.cpp:362 +msgid "(no recent files)" +msgstr "(žádné nedávné soubory)" + +#: graphicswin.cpp:370 +#, c-format +msgid "File '%s' does not exist." +msgstr "Soubor '%s' neexistuje." + +#: graphicswin.cpp:779 +msgid "No workplane is active, so the grid will not appear." +msgstr "Žádná pracovní rovina není aktivní, proto nebude mřížka zobrazena." + +#: graphicswin.cpp:794 +msgid "" +"The perspective factor is set to zero, so the view will always be a parallel " +"projection.\n" +"\n" +"For a perspective projection, modify the perspective factor in the " +"configuration screen. A value around 0.3 is typical." +msgstr "" +"Faktor perspektivy je nastaven na nulu, proto bude pohled vždy rovnoběžnou " +"projekcí.\n" +"\n" +"Pro perspektivní projekci uprav faktor perspektivy v konfigurační obrazovce. " +"Typická hodnota je kolem 0,3." + +#: graphicswin.cpp:884 +msgid "" +"Select a point; this point will become the center of the view on screen." +msgstr "Vyber bod; tento bod se stane středem pohledu na obrazovce." + +#: graphicswin.cpp:1193 +msgid "No additional entities share endpoints with the selected entities." +msgstr "Žádné další entity nesdílejí koncové body s vybranými entitami." + +#: graphicswin.cpp:1211 +msgid "" +"To use this command, select a point or other entity from an linked part, or " +"make a link group the active group." +msgstr "" +"ChceÅ¡-li použít tento příkaz, vyber bod nebo jinou entitu z propojené části " +"nebo vyber skupinu propojení jako aktivní skupinu." + +#: graphicswin.cpp:1234 +msgid "" +"No workplane is active. Activate a workplane (with Sketch -> In Workplane) " +"to define the plane for the snap grid." +msgstr "" +"Žádná pracovní rovina není aktivní. Aktivuj pracovní rovinu (pomocí Náčrt -> " +"V pracovní rovině) pro nastavení roviny přichytávání na mřížku." + +#: graphicswin.cpp:1241 +msgid "" +"Can't snap these items to grid; select points, text comments, or constraints " +"with a label. To snap a line, select its endpoints." +msgstr "" +"Tyto položky nelze přichytit k mřížce; vyber body, textové komentáře nebo " +"omezení se Å¡títkem. ChceÅ¡-li přichytit úsečku, vyber její koncové body." + +#: graphicswin.cpp:1326 +msgid "No workplane selected. Activating default workplane for this group." +msgstr "" +"Není vybrána žádná pracovní rovina. Aktivuji výchozí pracovní rovinu pro " +"tuto skupinu." + +#: graphicswin.cpp:1329 +msgid "" +"No workplane is selected, and the active group does not have a default " +"workplane. Try selecting a workplane, or activating a sketch-in-new-" +"workplane group." +msgstr "" +"Není vybrána žádná pracovní rovina a aktivní skupina nemá výchozí pracovní " +"rovinu přiřazenu. Zkus vybrat pracovní rovinu nebo aktivovat skupinu náčrt-v-" +"rovině." + +#: graphicswin.cpp:1350 +msgid "" +"Bad selection for tangent arc at point. Select a single point, or select " +"nothing to set up arc parameters." +msgstr "" +"Chybný výběr tečného oblouku v bodě. Vyber jeden bod nebo výběr zruÅ¡ pro " +"nastavení parametrů oblouku." + +#: graphicswin.cpp:1361 +msgid "click point on arc (draws anti-clockwise)" +msgstr "klikni na bod oblouku (kresleno proti směru hodinových ručiček)" + +#: graphicswin.cpp:1362 +msgid "click to place datum point" +msgstr "klikni pro umístění vztažného bodu" + +#: graphicswin.cpp:1363 +msgid "click first point of line segment" +msgstr "klikni na první bod úsečky" + +#: graphicswin.cpp:1365 +msgid "click first point of construction line segment" +msgstr "klikni na první bod konstrukční úsečky" + +#: graphicswin.cpp:1366 +msgid "click first point of cubic segment" +msgstr "klikni na první bod segmentu splajnu" + +#: graphicswin.cpp:1367 +msgid "click center of circle" +msgstr "klikni na střed kružnice" + +#: graphicswin.cpp:1368 +msgid "click origin of workplane" +msgstr "klikni na počátek pracovní roviny" + +#: graphicswin.cpp:1369 +msgid "click one corner of rectangle" +msgstr "klikni na jeden roh obdélníku" + +#: graphicswin.cpp:1370 +msgid "click top left of text" +msgstr "klikni na levý horní roh textu" + +#: graphicswin.cpp:1376 +msgid "click top left of image" +msgstr "klikni na levý horní roh obrázku" + +#: graphicswin.cpp:1402 +msgid "" +"No entities are selected. Select entities before trying to toggle their " +"construction state." +msgstr "" +"Nejsou vybrány žádné entity. Před přepnutím stavu konstrukce nějaké entity " +"vyber." + +#: group.cpp:86 +msgctxt "group-name" +msgid "sketch-in-3d" +msgstr "náčrt-ve-3D" + +#: group.cpp:154 +msgid "" +"Bad selection for new sketch in workplane. This group can be created with:\n" +"\n" +" * a point (through the point, orthogonal to coordinate axes)\n" +" * a point and two line segments (through the point, parallel to the " +"lines)\n" +" * a point and a normal (through the point, orthogonal to the normal)\n" +" * a workplane (copy of the workplane)\n" +msgstr "" +"Chybný výběr nového náčrtu v pracovním plánu. Tuto skupinu lze vytvořit " +"pomocí:\n" +"\n" +" * bodu (přes bod, kolmo na souřadnicové osy)\n" +" * bodu a dvou úseček (procházející bodem, rovnoběžná s přímkami)\n" +" * bodu a normály (procházející bodem, kolmá na normálu)\n" +" * pracovní roviny (kopie pracovní roviny)\n" + +#: group.cpp:170 +msgid "" +"Activate a workplane (Sketch -> In Workplane) before extruding. The sketch " +"will be extruded normal to the workplane." +msgstr "" +"Před extruzí aktivuj pracovní rovinu (Náčrt -> V pracovní rovině). Náčrt " +"bude extrudován ve směru normály k pracovní rovině." + +#: group.cpp:179 +msgctxt "group-name" +msgid "extrude" +msgstr "extruze" + +#: group.cpp:184 +msgid "Lathe operation can only be applied to planar sketches." +msgstr "Operace plné rotace lze použít pouze na rovinné náčrty.." + +#: group.cpp:195 +msgid "" +"Bad selection for new lathe group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Chybný výběr pro novou skupinu plné rotace. Tuto skupinu lze vytvořit " +"pomocí:\n" +"\n" +" * bodu a úsečky nebo normály (rotací kolem osy rovnoběžné k přímce / " +"normále, procházející bodem)\n" +" * úsečky (rotací kolem úsečky)\n" + +#: group.cpp:205 +msgctxt "group-name" +msgid "lathe" +msgstr "plná-rotace" + +#: group.cpp:210 +msgid "Revolve operation can only be applied to planar sketches." +msgstr "Operaci volné rotace lze použít pouze na rovinné náčrty." + +#: group.cpp:221 +msgid "" +"Bad selection for new revolve group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Chybný výběr pro novou skupinu volné rotace. Tuto skupinu lze vytvořit " +"pomocí:\n" +"\n" +" * bodu a úsečky nebo normály (rotace kolem osy rovnoběžné s přímkou / " +"normálou, procházející bodem)\n" +" * úsečky (rotace kolem úsečky)\n" + +#: group.cpp:233 +msgctxt "group-name" +msgid "revolve" +msgstr "volná-rotace" + +#: group.cpp:238 +msgid "Helix operation can only be applied to planar sketches." +msgstr "Operaci Å¡roubovice lze použít pouze na rovinné náčrty." + +#: group.cpp:249 +msgid "" +"Bad selection for new helix group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Chybný výběr pro novou skupinu Å¡roubovice. Tuto skupinu lze vytvořit " +"pomocí:\n" +"\n" +" * bodu a úsečky nebo normály (rotací kolem osy rovnoběžné k přímce / " +"normále, procházející bodem)\n" +" * úsečky (rotací kolem úsečky)\n" + +#: group.cpp:261 +msgctxt "group-name" +msgid "helix" +msgstr "Å¡roubovice" + +#: group.cpp:274 +msgid "" +"Bad selection for new rotation. This group can be created with:\n" +"\n" +" * a point, while locked in workplane (rotate in plane, about that " +"point)\n" +" * a point and a line or a normal (rotate about an axis through the " +"point, and parallel to line / normal)\n" +msgstr "" +"Chybný výběr pro nové krokové otočení. Tuto skupinu lze vytvořit pomocí:\n" +"\n" +" * bodu, přičemž je uzamčen v pracovní rovině (otočení v rovině, kolem " +"tohoto bodu)\n" +" * bodu a úsečky nebo normály (otočení kolem osy procházející bodem a " +"rovnoběžně s přímkou / normálou)\n" + +#: group.cpp:287 +msgctxt "group-name" +msgid "rotate" +msgstr "otočení" + +#: group.cpp:298 +msgctxt "group-name" +msgid "translate" +msgstr "posun" + +#: group.cpp:422 +msgid "(unnamed)" +msgstr "(nepojmenované)" + +#: groupmesh.cpp:710 +msgid "not closed contour, or not all same style!" +msgstr "Obrys není uzavřený nebo není celý v jednotném stylu!" + +#: groupmesh.cpp:723 +msgid "points not all coplanar!" +msgstr "Ne vÅ¡echny body jsou v rovině!" + +#: groupmesh.cpp:725 +msgid "contour is self-intersecting!" +msgstr "Obrys se sám protíná!" + +#: groupmesh.cpp:727 +msgid "zero-length edge!" +msgstr "Nulová délka hrany!" + +#: importmesh.cpp:136 +msgid "Text-formated STL files are not currently supported" +msgstr "Textové soubory STL nejsou v této chvíli podporovány." + +#: modify.cpp:252 +msgid "Must be sketching in workplane to create tangent arc." +msgstr "Pro vytvoření tečného oblouku je nutné kreslit v pracovní rovině." + +#: modify.cpp:299 +msgid "" +"To create a tangent arc, select a point where two non-construction lines or " +"circles in this group and workplane join." +msgstr "" +"ChceÅ¡-li vytvořit tečný oblouk, vyber bod, kde se s pracovní rovinou stýkají " +"dvě nekonstrukční úsečky nebo kružnice v této skupině." + +#: modify.cpp:386 +msgid "" +"Couldn't round this corner. Try a smaller radius, or try creating the " +"desired geometry by hand with tangency constraints." +msgstr "" +"Tento roh nelze zaoblit. Zkus menší poloměr nebo zkus vytvořit požadovanou " +"geometrii ručně pomocí tečných omezení." + +#: modify.cpp:595 +msgid "Couldn't split this entity; lines, circles, or cubics only." +msgstr "" +"Tuto entitu se nepodařilo rozdělit; pouze úsečky, kružnice nebo splajny." + +#: modify.cpp:622 +msgid "Must be sketching in workplane to split." +msgstr "Rozdělení lze provést pouze při náčrtu v pracovní rovině." + +#: modify.cpp:629 +msgid "" +"Select two entities that intersect each other (e.g. two lines/circles/arcs " +"or a line/circle/arc and a point)." +msgstr "" +"Vyber dvě entity, které se vzájemně protínají (např. dvě čáry / kružnice / " +"oblouky nebo úsečka / kružnice / oblouk a bod)." + +#: modify.cpp:734 +msgid "Can't split; no intersection found." +msgstr "Nelze rozdělit, nebyla nalezena žádná průsečnice." + +#: mouse.cpp:558 +msgid "Assign to Style" +msgstr "Přiřadit ke stylu" + +#: mouse.cpp:574 +msgid "No Style" +msgstr "Žádný styl" + +#: mouse.cpp:577 +msgid "Newly Created Custom Style..." +msgstr "Nově vytvořený vlastní styl..." + +#: mouse.cpp:584 +msgid "Group Info" +msgstr "Info o skupině" + +#: mouse.cpp:604 +msgid "Style Info" +msgstr "Info o stylu" + +#: mouse.cpp:624 +msgid "Select Edge Chain" +msgstr "Vybrat řetězec hran" + +#: mouse.cpp:630 +msgid "Toggle Reference Dimension" +msgstr "Přepnout referenční rozměr" + +#: mouse.cpp:636 +msgid "Other Supplementary Angle" +msgstr "Další doplňkový úhel" + +#: mouse.cpp:641 +msgid "Snap to Grid" +msgstr "Přichytit k mřížce" + +#: mouse.cpp:650 +msgid "Remove Spline Point" +msgstr "Odebrat bod splajnu" + +#: mouse.cpp:685 +msgid "Add Spline Point" +msgstr "Přidat bod splajnu" + +#: mouse.cpp:689 +msgid "Cannot add spline point: maximum number of points reached." +msgstr "Nelze přidat bod splajnu: bylo dosaženo maximálního počtu bodů." + +#: mouse.cpp:714 +msgid "Toggle Construction" +msgstr "Přepnout konstrukci" + +#: mouse.cpp:730 +msgid "Delete Point-Coincident Constraint" +msgstr "Odstranit omezení kolidujícího bodu" + +#: mouse.cpp:748 +msgid "Cut" +msgstr "Vyjmout" + +#: mouse.cpp:750 +msgid "Copy" +msgstr "Kopírovat" + +#: mouse.cpp:754 +msgid "Select All" +msgstr "Vybrat vÅ¡e" + +#: mouse.cpp:759 +msgid "Paste" +msgstr "Vložit" + +#: mouse.cpp:761 +msgid "Paste Transformed..." +msgstr "Vložit transformované..." + +#: mouse.cpp:766 +msgid "Delete" +msgstr "Odstranit" + +#: mouse.cpp:769 +msgid "Unselect All" +msgstr "ZruÅ¡it výběr vÅ¡eho" + +#: mouse.cpp:776 +msgid "Unselect Hovered" +msgstr "ZruÅ¡it výběr při najetí" + +#: mouse.cpp:785 +msgid "Zoom to Fit" +msgstr "Přiblížit na míru" + +#: mouse.cpp:987 mouse.cpp:1276 +msgid "click next point of line, or press Esc" +msgstr "klikni na další bod úsečky nebo stiskni klávesu Esc" + +#: mouse.cpp:993 +msgid "" +"Can't draw rectangle in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Nelze nakreslit obdélník ve 3D; nejprve aktivuj pracovní rovinu pomocí Náčrt " +"-> V pracovní rovině." + +#: mouse.cpp:1027 +msgid "click to place other corner of rectangle" +msgstr "kliknutím umísti další roh obdélníku" + +#: mouse.cpp:1048 +msgid "click to set radius" +msgstr "kliknutím nastav poloměr" + +#: mouse.cpp:1053 +msgid "" +"Can't draw arc in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Nelze nakreslit oblouk ve 3D; nejprve aktivuj pracovní rovinu pomocí Náčrt -" +"> V pracovní rovině." + +#: mouse.cpp:1072 +msgid "click to place point" +msgstr "kliknutím umísti bod" + +#: mouse.cpp:1088 +msgid "click next point of cubic, or press Esc" +msgstr "klikni na další bod splajnu nebo stiskni Esc" + +#: mouse.cpp:1093 +msgid "" +"Sketching in a workplane already; sketch in 3d before creating new workplane." +msgstr "" +"Kreslení v pracovní rovině již probíhá; před vytvořením nové pracovní roviny " +"kresli ve 3D." + +#: mouse.cpp:1109 +msgid "" +"Can't draw text in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Nelze kreslit text ve 3D; nejprve aktivuj pracovní rovinu pomocí Náčrt -> V " +"pracovní rovině." + +#: mouse.cpp:1126 +msgid "click to place bottom right of text" +msgstr "klikni pro umístění pravého dolního rohu textu" + +#: mouse.cpp:1132 +msgid "" +"Can't draw image in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Nelze nakreslit obrázek ve 3d; nejprve aktivuj pracovní rovinu pomocí Náčrt -" +"> V pracovní rovině." + +#: platform/gui.cpp:85 platform/gui.cpp:90 solvespace.cpp:583 +msgctxt "file-type" +msgid "SolveSpace models" +msgstr "SolveSpace modely" + +#: platform/gui.cpp:89 +msgctxt "file-type" +msgid "ALL" +msgstr "VÅ E" + +#: platform/gui.cpp:91 +msgctxt "file-type" +msgid "IDF circuit board" +msgstr "IDF deska ploÅ¡ných spojů" + +#: platform/gui.cpp:92 +msgctxt "file-type" +msgid "STL triangle mesh" +msgstr "STL trojúhelníková síť" + +#: platform/gui.cpp:96 +msgctxt "file-type" +msgid "PNG image" +msgstr "PNG obrázek" + +#: platform/gui.cpp:100 +msgctxt "file-type" +msgid "STL mesh" +msgstr "STL síť (mesh)" + +#: platform/gui.cpp:101 +msgctxt "file-type" +msgid "Wavefront OBJ mesh" +msgstr "Wavefront OBJ síť (mesh)" + +#: platform/gui.cpp:102 +msgctxt "file-type" +msgid "Three.js-compatible mesh, with viewer" +msgstr "Three.js-compatibilní síť (mesh), s prohlížečem" + +#: platform/gui.cpp:103 +msgctxt "file-type" +msgid "Three.js-compatible mesh, mesh only" +msgstr "Three.js-compatibilní síť (mesh), bez prohlížeče" + +#: platform/gui.cpp:104 +msgctxt "file-type" +msgid "VRML text file" +msgstr "VRML textový soubor" + +#: platform/gui.cpp:108 platform/gui.cpp:115 platform/gui.cpp:122 +msgctxt "file-type" +msgid "STEP file" +msgstr "STEP soubor" + +#: platform/gui.cpp:112 +msgctxt "file-type" +msgid "PDF file" +msgstr "PDF soubor" + +#: platform/gui.cpp:113 +msgctxt "file-type" +msgid "Encapsulated PostScript" +msgstr "Zapouzdřený PostScript" + +#: platform/gui.cpp:114 +msgctxt "file-type" +msgid "Scalable Vector Graphics" +msgstr "SVG soubor" + +#: platform/gui.cpp:116 platform/gui.cpp:123 +msgctxt "file-type" +msgid "DXF file (AutoCAD 2007)" +msgstr "DXF soubor (AutoCAD 2007)" + +#: platform/gui.cpp:117 +msgctxt "file-type" +msgid "HPGL file" +msgstr "HPGL soubor" + +#: platform/gui.cpp:118 +msgctxt "file-type" +msgid "G Code" +msgstr "G kód (G Code)" + +#: platform/gui.cpp:127 +msgctxt "file-type" +msgid "AutoCAD DXF and DWG files" +msgstr "AutoCAD DXF a DWG soubory" + +#: platform/gui.cpp:131 +msgctxt "file-type" +msgid "Comma-separated values" +msgstr "CSV soubor" + +#: platform/guigtk.cpp:1434 platform/guimac.mm:1513 platform/guiwin.cpp:1641 +msgid "untitled" +msgstr "nepojmenovaný" + +#: platform/guigtk.cpp:1445 platform/guigtk.cpp:1481 platform/guimac.mm:1471 +#: platform/guiwin.cpp:1639 +msgctxt "title" +msgid "Save File" +msgstr "Uložit soubor" + +#: platform/guigtk.cpp:1446 platform/guigtk.cpp:1482 platform/guimac.mm:1454 +#: platform/guiwin.cpp:1645 +msgctxt "title" +msgid "Open File" +msgstr "Otevřít soubor" + +#: platform/guigtk.cpp:1449 platform/guigtk.cpp:1488 +msgctxt "button" +msgid "_Cancel" +msgstr "_ZruÅ¡it" + +#: platform/guigtk.cpp:1450 platform/guigtk.cpp:1486 +msgctxt "button" +msgid "_Save" +msgstr "_Uložit" + +#: platform/guigtk.cpp:1451 platform/guigtk.cpp:1487 +msgctxt "button" +msgid "_Open" +msgstr "_Otevřít" + +#: solvespace.cpp:175 +msgctxt "title" +msgid "Autosave Available" +msgstr "Dostupné automatické ukládání" + +#: solvespace.cpp:176 +msgctxt "dialog" +msgid "An autosave file is available for this sketch." +msgstr "Pro tento náčrt je k dispozici automatické ukládání souboru." + +#: solvespace.cpp:177 +msgctxt "dialog" +msgid "Do you want to load the autosave file instead?" +msgstr "ChceÅ¡ místo toho načíst automaticky uložený soubor?" + +#: solvespace.cpp:178 +msgctxt "button" +msgid "&Load autosave" +msgstr "&Načíst automaticky uložený" + +#: solvespace.cpp:180 +msgctxt "button" +msgid "Do&n't Load" +msgstr "&Nenačítat" + +#: solvespace.cpp:640 +msgctxt "title" +msgid "Modified File" +msgstr "Upravený soubor" + +#: solvespace.cpp:642 +#, c-format +msgctxt "dialog" +msgid "Do you want to save the changes you made to the sketch “%s”?" +msgstr "ChceÅ¡ uložit změny, které jsi provedl v náčrtu “%s”?" + +#: solvespace.cpp:645 +msgctxt "dialog" +msgid "Do you want to save the changes you made to the new sketch?" +msgstr "ChceÅ¡ provedené změny uložit do nového náčrtu?" + +#: solvespace.cpp:648 +msgctxt "dialog" +msgid "Your changes will be lost if you don't save them." +msgstr "Pokud změny neuložíš, budou ztraceny." + +#: solvespace.cpp:649 +msgctxt "button" +msgid "&Save" +msgstr "&Uložit" + +#: solvespace.cpp:651 +msgctxt "button" +msgid "Do&n't Save" +msgstr "&Neukládat" + +#: solvespace.cpp:672 +msgctxt "title" +msgid "(new sketch)" +msgstr "(nový náčrt)" + +#: solvespace.cpp:683 +msgctxt "title" +msgid "Property Browser" +msgstr "Parametry" + +#: solvespace.cpp:746 +msgid "" +"Constraints are currently shown, and will be exported in the toolpath. This " +"is probably not what you want; hide them by clicking the link at the top of " +"the text window." +msgstr "" +"Omezení jsou aktuálně zobrazena a budou exportována do cesty nástroje. To " +"pravděpodobně není to, co chceÅ¡; skryj je kliknutím na odkaz v horní části " +"textového okna." + +#: solvespace.cpp:834 +#, c-format +msgid "" +"Can't identify file type from file extension of filename '%s'; try .dxf or ." +"dwg." +msgstr "" +"Nelze určit typ souboru podle přípony názvu souboru '%s'; zkus .dxf nebo ." +"dwg." + +#: solvespace.cpp:886 +msgid "Constraint must have a label, and must not be a reference dimension." +msgstr "Omezení musí mít popisek a nesmí být referenčním rozměrem." + +#: solvespace.cpp:890 +msgid "Bad selection for step dimension; select a constraint." +msgstr "Chybný výběr rozměru kroku; vyber omezení." + +#: solvespace.cpp:914 +msgid "The assembly does not interfere, good." +msgstr "V pořádku, sestava se nepřekrývá." + +#: solvespace.cpp:930 +#, c-format +msgid "" +"The volume of the solid model is:\n" +"\n" +" %s" +msgstr "" +"Objem modelu tělesa je:\n" +"\n" +" %s" + +#: solvespace.cpp:939 +#, c-format +msgid "" +"\n" +"The volume of current group mesh is:\n" +"\n" +" %s" +msgstr "" +"\n" +"Objem aktuální mesh skupiny je:\n" +"\n" +" %s" + +#: solvespace.cpp:944 +msgid "" +"\n" +"\n" +"Curved surfaces have been approximated as triangles.\n" +"This introduces error, typically of around 1%." +msgstr "" +"\n" +"\n" +"Zakřivené plochy byly aproximovány jako trojúhelníky.\n" +"To přináší chybu, obvykle kolem 1%%." + +#: solvespace.cpp:959 +#, c-format +msgid "" +"The surface area of the selected faces is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"Plocha vybraných stěn je:\n" +"\n" +" %s\n" +"\n" +"Křivky byly aproximovány jako po částech lineární.\n" +"To přináší chybu, obvykle kolem 1%%." + +#: solvespace.cpp:968 +msgid "" +"This group does not contain a correctly-formed 2d closed area. It is open, " +"not coplanar, or self-intersecting." +msgstr "" +"Tato skupina neobsahuje správně vytvořenou uzavřenou 2D oblast. Je otevřená, " +"není koplanární ani se neprotíná." + +#: solvespace.cpp:980 +#, c-format +msgid "" +"The area of the region sketched in this group is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"Plocha regionu zakresleného v této skupině je:\n" +"\n" +" %s\n" +"\n" +"Křivky byly aproximovány jako po částech lineární.\n" +"To přináší chybu, obvykle kolem 1%%." + +#: solvespace.cpp:1000 +#, c-format +msgid "" +"The total length of the selected entities is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"Celková délka vybraných entit je:\n" +"\n" +" %s\n" +"\n" +"Křivky byly aproximovány jako po částech lineární.\n" +"To přináší chybu, obvykle kolem 1%%." + +#: solvespace.cpp:1006 +msgid "Bad selection for perimeter; select line segments, arcs, and curves." +msgstr "Chybný výběr obvodu; vyber úsečky, oblouky a křivky." + +#: solvespace.cpp:1022 +msgid "Bad selection for trace; select a single point." +msgstr "Chybný výběr pro trasování; vyber jeden bod." + +#: solvespace.cpp:1049 +#, c-format +msgid "Couldn't write to '%s'" +msgstr "Nelze zapisovat do '%s'" + +#: solvespace.cpp:1079 +msgid "The mesh is self-intersecting (NOT okay, invalid)." +msgstr "Síť se sama protíná (NENÍ v pořádku, je neplatná)." + +#: solvespace.cpp:1080 +msgid "The mesh is not self-intersecting (okay, valid)." +msgstr "Síť se sama neprotíná (je v pořádku, je platná)." + +#: solvespace.cpp:1082 +msgid "The mesh has naked edges (NOT okay, invalid)." +msgstr "Síť má obnažené hrany (NENÍ v pořádku, je neplatná)." + +#: solvespace.cpp:1083 +msgid "The mesh is watertight (okay, valid)." +msgstr "Síť je vodotěsná (je v pořádku, je platná)." + +#: solvespace.cpp:1086 +#, c-format +msgid "" +"\n" +"\n" +"The model contains %d triangles, from %d surfaces." +msgstr "" +"\n" +"\n" +"Model obsahuje %d trojúhelníků z %d povrchů." + +#: solvespace.cpp:1090 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"Zero problematic edges, good.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"Žádné problematické hrany, v pořádku.%s" + +#: solvespace.cpp:1093 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d problematic edges, bad.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"Chyba, počet problematických hran: %d.%s" + +#: solvespace.cpp:1106 +#, c-format +msgid "" +"This is SolveSpace version %s.\n" +"\n" +"For more information, see http://solvespace.com/\n" +"\n" +"SolveSpace is free software: you are free to modify\n" +"and/or redistribute it under the terms of the GNU\n" +"General Public License (GPL) version 3 or later.\n" +"\n" +"There is NO WARRANTY, to the extent permitted by\n" +"law. For details, visit http://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues and other authors.\n" +msgstr "" +"Toto je SolveSpace verze %s.\n" +"\n" +"Další informace naleznete na adrese http://solvespace.com/\n" +"\n" +"SolveSpace je svobodný software: můžete jej svobodně upravovat\n" +"a/nebo jej šířit za podmínek GNU\n" +"General Public License (GPL) verze 3 nebo novější.\n" +"\n" +"V rozsahu povoleném zákonem není poskytována ŽÁDNÁ ZÁRUKA.\n" +"Podrobnosti najdete na adrese http://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues a další autoři.\n" + +#: style.cpp:185 +msgid "" +"Can't assign style to an entity that's derived from another entity; try " +"assigning a style to this entity's parent." +msgstr "" +"Nelze přiřadit styl entitě, která je odvozena od jiné entity; zkus přiřadit " +"styl nadřazené entitě." + +#: style.cpp:735 +msgid "Style name cannot be empty" +msgstr "Název stylu nemůže být prázdný" + +#: textscreens.cpp:837 +msgid "Can't repeat fewer than 1 time." +msgstr "Nelze opakovat méně než 1krát." + +#: textscreens.cpp:841 +msgid "Can't repeat more than 999 times." +msgstr "Nelze opakovat více než 999krát." + +#: textscreens.cpp:866 +msgid "Group name cannot be empty" +msgstr "Název skupiny nemůže být prázdný" + +#: textscreens.cpp:918 +msgid "Opacity must be between zero and one." +msgstr "Neprůhlednost musí být mezi nulou a jedničkou." + +#: textscreens.cpp:953 +msgid "Radius cannot be zero or negative." +msgstr "Poloměr nemůže být nulový nebo záporný." + +#: toolbar.cpp:18 +msgid "Sketch line segment" +msgstr "Náčrt úsečky" + +#: toolbar.cpp:20 +msgid "Sketch rectangle" +msgstr "Náčrt obdélníku" + +#: toolbar.cpp:22 +msgid "Sketch circle" +msgstr "Náčrt kružnice" + +#: toolbar.cpp:24 +msgid "Sketch arc of a circle" +msgstr "Náčrt oblouku kružnice" + +#: toolbar.cpp:26 +msgid "Sketch curves from text in a TrueType font" +msgstr "Náčrt křivek z textu v písmu TrueType" + +#: toolbar.cpp:28 +msgid "Sketch image from a file" +msgstr "Náčrt obrázku ze souboru" + +#: toolbar.cpp:30 +msgid "Create tangent arc at selected point" +msgstr "Vytvořit tečný oblouk ve vybraném bodě" + +#: toolbar.cpp:32 +msgid "Sketch cubic Bezier spline" +msgstr "Náčrt kubického Bézierova splajnu" + +#: toolbar.cpp:34 +msgid "Sketch datum point" +msgstr "Náčrt vztažného bodu" + +#: toolbar.cpp:36 +msgid "Toggle construction" +msgstr "Přepnout konstrukci" + +#: toolbar.cpp:38 +msgid "Split lines / curves where they intersect" +msgstr "Rozdělení úseček / křivek v místě jejich průsečíku" + +#: toolbar.cpp:42 +msgid "Constrain distance / diameter / length" +msgstr "Omezení vzdálenosti / průměru / délky" + +#: toolbar.cpp:44 +msgid "Constrain angle" +msgstr "Omezení úhlu" + +#: toolbar.cpp:46 +msgid "Constrain to be horizontal" +msgstr "Omezení horizontály" + +#: toolbar.cpp:48 +msgid "Constrain to be vertical" +msgstr "Omezení vertikály" + +#: toolbar.cpp:50 +msgid "Constrain to be parallel or tangent" +msgstr "Omezení rovnoběžnosti nebo tečny" + +#: toolbar.cpp:52 +msgid "Constrain to be perpendicular" +msgstr "Omezení kolmosti" + +#: toolbar.cpp:54 +msgid "Constrain point on line / curve / plane / point" +msgstr "Omezení bodu na přímce / křivce / rovině / bodu" + +#: toolbar.cpp:56 +msgid "Constrain symmetric" +msgstr "Omezení symetrie" + +#: toolbar.cpp:58 +msgid "Constrain equal length / radius / angle" +msgstr "Omezení shodné délky / poloměru / úhlu" + +#: toolbar.cpp:60 +msgid "Constrain normals in same orientation" +msgstr "Omezení normál se stejnou orientací" + +#: toolbar.cpp:62 +msgid "Other supplementary angle" +msgstr "Další doplňkový úhel" + +#: toolbar.cpp:64 +msgid "Toggle reference dimension" +msgstr "Přepnout referenční rozměr" + +#: toolbar.cpp:68 +msgid "New group extruding active sketch" +msgstr "Nová skupina extruzí aktivního náčrtu" + +#: toolbar.cpp:70 +msgid "New group rotating active sketch" +msgstr "Nová skupina plnou rotací aktivního náčrtu" + +#: toolbar.cpp:72 +msgid "New group helix from active sketch" +msgstr "Nová skupina Å¡roubovicí aktivního náčrtu" + +#: toolbar.cpp:74 +msgid "New group revolve active sketch" +msgstr "Nová skupina volnou rotací aktivního náčrtu" + +#: toolbar.cpp:76 +msgid "New group step and repeat rotating" +msgstr "Nová skupina krokovým otočením" + +#: toolbar.cpp:78 +msgid "New group step and repeat translating" +msgstr "Nová skupina krokovým posunem" + +#: toolbar.cpp:80 +msgid "New group in new workplane (thru given entities)" +msgstr "Nová skupina v pracovní rovině (přes dané entity)" + +#: toolbar.cpp:82 +msgid "New group in 3d" +msgstr "Nová skupina ve 3D" + +#: toolbar.cpp:84 +msgid "New group linking / assembling file" +msgstr "Nová skupina odkazem / sestavením souboru" + +#: toolbar.cpp:88 +msgid "Nearest isometric view" +msgstr "Nejbližší isometrický pohled" + +#: toolbar.cpp:90 +msgid "Align view to active workplane" +msgstr "Zarovnat pohled na aktivní pracovní rovinu" + +#: util.cpp:165 +msgctxt "title" +msgid "Error" +msgstr "Chyba" + +#: util.cpp:165 +msgctxt "title" +msgid "Message" +msgstr "Zpráva" + +#: util.cpp:170 +msgctxt "button" +msgid "&OK" +msgstr "&OK" + +#: view.cpp:127 +msgid "Scale cannot be zero or negative." +msgstr "Měřítko nemůže být nulové nebo záporné." + +#: view.cpp:139 view.cpp:148 +msgid "Bad format: specify x, y, z" +msgstr "Chybný formát: zadej x, y, z" diff --git a/res/locales/de_DE.po b/res/locales/de_DE.po new file mode 100644 index 0000000..ab509c3 --- /dev/null +++ b/res/locales/de_DE.po @@ -0,0 +1,2617 @@ +# German translations for the SolveSpace package. +# Copyright (C) 2018 the SolveSpace authors +# This file is distributed under the same license as the SolveSpace package. +# Guido Hoss , 2018. #zanata +msgid "" +msgstr "" +"Project-Id-Version: SolveSpace 3.0\n" +"Report-Msgid-Bugs-To: phkahler@gmail.com\n" +"POT-Creation-Date: 2025-01-26 21:04+0200\n" +"PO-Revision-Date: 2022-04-30 16:44+0200\n" +"Last-Translator: Reini Urban \n" +"Language-Team: none\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: clipboard.cpp:314 +msgid "" +"Cut, paste, and copy work only in a workplane.\n" +"\n" +"Activate one with Sketch -> In Workplane." +msgstr "" +"Ausschneiden, Einfügen und Kopieren sind nur in einer Arbeitsebene " +"zulässig.\n" +"\n" +"Aktivieren Sie eine mit \"Skizze -> In Arbeitsebene\"." + +#: clipboard.cpp:331 +msgid "Clipboard is empty; nothing to paste." +msgstr "Zwischenablage ist leer; es gibt nichts einzufügen." + +#: clipboard.cpp:378 +msgid "Number of copies to paste must be at least one." +msgstr "Die Anzahl der einzufügenden Kopien muss mind. 1 sein." + +#: clipboard.cpp:394 textscreens.cpp:879 +msgid "Scale cannot be zero." +msgstr "Maßstab kann nicht Null sein." + +#: clipboard.cpp:436 +msgid "Select one point to define origin of rotation." +msgstr "Wählen Sie einen Punkt, um den Drehmittelpunkt zu definieren." + +#: clipboard.cpp:448 +msgid "Select two points to define translation vector." +msgstr "Wählen Sie zwei Punkte, um den Verschiebungsvektor zu definieren." + +#: clipboard.cpp:458 +msgid "" +"Transformation is identity. So all copies will be exactly on top of each " +"other." +msgstr "" +"Die Transformation ist die Identität. Alle Kopien werden deckungsgleich " +"übereinanderliegen." + +#: clipboard.cpp:462 +msgid "Too many items to paste; split this into smaller pastes." +msgstr "" +"Zuviele Objekte zum Einfügen; teilen Sie diese in kleinere " +"Einfügeoperationen auf." + +#: clipboard.cpp:467 +msgid "No workplane active." +msgstr "Es ist keine Arbeitsebene aktiv." + +#: confscreen.cpp:410 +msgid "Bad format: specify coordinates as x, y, z" +msgstr "Ungültiges Format: geben Sie Koordinaten als x, y, z an" + +#: confscreen.cpp:420 style.cpp:729 textscreens.cpp:910 +msgid "Bad format: specify color as r, g, b" +msgstr "Ungültiges Format: geben Sie Farben als r, g, b an" + +#: confscreen.cpp:446 +msgid "" +"The perspective factor will have no effect until you enable View -> Use " +"Perspective Projection." +msgstr "" +"Der Perspektivfaktor wird sich nicht auswirken, bis Sie Ansicht -> " +"Perspektive Projektion aktivieren." + +#: confscreen.cpp:464 confscreen.cpp:474 +#, c-format +msgid "Specify between 0 and %d digits after the decimal." +msgstr "Geben Sie 0 bis %d Ziffern nach dem Dezimalzeichen an." + +#: confscreen.cpp:486 +msgid "Export scale must not be zero!" +msgstr "Der Exportmaßstab darf nicht Null sein!" + +#: confscreen.cpp:498 +msgid "Cutter radius offset must not be negative!" +msgstr "Der Werkzeugradialabstand darf nicht negativ sein!" + +#: confscreen.cpp:557 +msgid "Bad value: autosave interval should be positive" +msgstr "" +"Ungültiger Wert: Interval für automatisches Speichern muss positiv sein" + +#: confscreen.cpp:560 +msgid "Bad format: specify interval in integral minutes" +msgstr "Ungültiges Format: geben Sie das Interval in ganzen Minuten an" + +#: constraint.cpp:12 +msgctxt "constr-name" +msgid "pts-coincident" +msgstr "Pkt-Deckung" + +#: constraint.cpp:13 +msgctxt "constr-name" +msgid "pt-pt-distance" +msgstr "Pkt-Pkt-Abstand" + +#: constraint.cpp:14 +msgctxt "constr-name" +msgid "pt-line-distance" +msgstr "Pkt-Linie-Abstand" + +#: constraint.cpp:15 +msgctxt "constr-name" +msgid "pt-plane-distance" +msgstr "Pkt-Ebene-Abstand" + +#: constraint.cpp:16 +msgctxt "constr-name" +msgid "pt-face-distance" +msgstr "Pkt-Fläche-Abstand" + +#: constraint.cpp:17 +msgctxt "constr-name" +msgid "proj-pt-pt-distance" +msgstr "Proj-Pkt-Pkt-Abstand" + +#: constraint.cpp:18 +msgctxt "constr-name" +msgid "pt-in-plane" +msgstr "Pkt-in-Ebene" + +#: constraint.cpp:19 +msgctxt "constr-name" +msgid "pt-on-line" +msgstr "Pkt-auf-Linie" + +#: constraint.cpp:20 +msgctxt "constr-name" +msgid "pt-on-face" +msgstr "Pkt-auf-Fläche" + +#: constraint.cpp:21 +msgctxt "constr-name" +msgid "eq-length" +msgstr "gl-Länge" + +#: constraint.cpp:22 +msgctxt "constr-name" +msgid "eq-length-and-pt-ln-dist" +msgstr "gl-Länge-und-Pkt-Linie-Abstand" + +#: constraint.cpp:23 +msgctxt "constr-name" +msgid "eq-pt-line-distances" +msgstr "gl-Pkt-Linie-Abstände" + +#: constraint.cpp:24 +msgctxt "constr-name" +msgid "length-ratio" +msgstr "Längenverhältnis" + +#: constraint.cpp:25 +msgctxt "constr-name" +msgid "arc-arc-length-ratio" +msgstr "Bogen-Bogen-Längenverhältnis" + +#: constraint.cpp:26 +msgctxt "constr-name" +msgid "arc-line-length-ratio" +msgstr "Bogen-Linien-Längenverhältnis" + +#: constraint.cpp:27 +msgctxt "constr-name" +msgid "length-difference" +msgstr "Längendifferenz" + +#: constraint.cpp:28 +msgctxt "constr-name" +msgid "arc-arc-len-difference" +msgstr "Bogen-Bogen-Längendifferenz" + +#: constraint.cpp:29 +msgctxt "constr-name" +msgid "arc-line-len-difference" +msgstr "Bogen-Linien-Längendifferenz" + +#: constraint.cpp:30 +msgctxt "constr-name" +msgid "symmetric" +msgstr "Symmetrisch" + +#: constraint.cpp:31 +msgctxt "constr-name" +msgid "symmetric-h" +msgstr "Symmetrisch-H" + +#: constraint.cpp:32 +msgctxt "constr-name" +msgid "symmetric-v" +msgstr "Symmetrisch-V" + +#: constraint.cpp:33 +msgctxt "constr-name" +msgid "symmetric-line" +msgstr "Symmetrisch-Linie" + +#: constraint.cpp:34 +msgctxt "constr-name" +msgid "at-midpoint" +msgstr "auf-Mittelpunkt" + +#: constraint.cpp:35 +msgctxt "constr-name" +msgid "horizontal" +msgstr "Horizontal" + +#: constraint.cpp:36 +msgctxt "constr-name" +msgid "vertical" +msgstr "Vertikal" + +#: constraint.cpp:37 +msgctxt "constr-name" +msgid "diameter" +msgstr "Durchmesser" + +#: constraint.cpp:38 +msgctxt "constr-name" +msgid "pt-on-circle" +msgstr "Pkt-auf-Kreis" + +#: constraint.cpp:39 +msgctxt "constr-name" +msgid "same-orientation" +msgstr "gl-Orientierung" + +#: constraint.cpp:40 +msgctxt "constr-name" +msgid "angle" +msgstr "Winkel" + +#: constraint.cpp:41 +msgctxt "constr-name" +msgid "parallel" +msgstr "Parallel" + +#: constraint.cpp:42 +msgctxt "constr-name" +msgid "arc-line-tangent" +msgstr "Bogen-Linie-Tangente" + +#: constraint.cpp:43 +msgctxt "constr-name" +msgid "cubic-line-tangent" +msgstr "Kub-Linie-Tangente" + +#: constraint.cpp:44 +msgctxt "constr-name" +msgid "curve-curve-tangent" +msgstr "Kurve-Kurve-Tangente" + +#: constraint.cpp:45 +msgctxt "constr-name" +msgid "perpendicular" +msgstr "Rechtwinklig" + +#: constraint.cpp:46 +msgctxt "constr-name" +msgid "eq-radius" +msgstr "gl-Radius" + +#: constraint.cpp:47 +msgctxt "constr-name" +msgid "eq-angle" +msgstr "gl-Winkel" + +#: constraint.cpp:48 +msgctxt "constr-name" +msgid "eq-line-len-arc-len" +msgstr "gl-Linie-Länge-Bogen-Länge" + +#: constraint.cpp:49 +msgctxt "constr-name" +msgid "lock-where-dragged" +msgstr "Fix-an-Position" + +#: constraint.cpp:50 +msgctxt "constr-name" +msgid "comment" +msgstr "Kommentar" + +#: constraint.cpp:151 +msgid "" +"The point you selected does not belong to the arc. The arc and line segment " +"do not share an end point.\n" +"\n" +"Select the end point of the arc at which you want it to be tangent to the " +"line." +msgstr "" +"Der ausgewählte Punkt gehört nicht zum Bogen. Der Bogen und das " +"Liniensegment haben keinen gemeinsamen Endpunkt.\n" +"\n" +"Wählen Sie den Endpunkt des Bogens aus, an dem Sie ihn tangential zur Linie " +"anpassen möchten." + +#: constraint.cpp:158 +msgid "" +"The tangent arc and line segment must share an endpoint. Constrain them with " +"Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the arc at which you want it to be " +"tangent to the line." +msgstr "" +"Die Bogentangente und das Liniensegment müssen einen gemeinsamen Endpunkt " +"haben. Schränken Sie mit \"Einschränkung / Auf Punkt\" ein, bevor Sie die " +"Tangente einschränken.\n" +"\n" +"Alternativ können Sie den Endpunkt des Bogens auswählen, an dem Sie die " +"Tangente an die Linie anpassen möchten." + +#: constraint.cpp:186 +msgid "" +"The point you selected is not an end point of the cubic spline. The spline " +"and line segment do not share an end point.\n" +"\n" +"Select the end point of the spline at which you want it to be tangent to the " +"line." +msgstr "" +"Der ausgewählte Punkt ist kein Endpunkt der kubischen Spline. Die Spline und " +"das Liniensegment haben keinen gemeinsamen Endpunkt.\n" +"\n" +"Wählen Sie den Endpunkt der Spline aus, an dem Sie sie tangential zur Linie " +"anpassen möchten." + +#: constraint.cpp:193 +msgid "" +"The tangent cubic spline and line segment must share an endpoint. Constrain " +"them with Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the cubic spline at which you want it " +"to be tangent to the line." +msgstr "" +"Die Kurventangente und das Liniensegment müssen einen gemeinsamen Endpunkt " +"haben. Schränken Sie mit \"Einschränkung / Auf Punkt\" ein, bevor Sie die " +"Tangente einschränken.\n" +"\n" +"Alternativ können Sie den Endpunkt der kubischen Spline auswählen, an dem " +"Sie die Tangente an die Linie anpassen möchten." + +#: constraint.cpp:237 +msgid "" +"The points you selected are not end points of the two curves. The curves do " +"not share an end point.\n" +"\n" +"Select the end points of both curves at which you want them to be tangent to " +"each other." +msgstr "" +"Die ausgewählten Punkte sind keine Endpunkte der beiden Kurven. Die Kurven " +"haben keinen gemeinsamen Endpunkt.\n" +"\n" +"Wählen Sie die Endpunkte beider Kurven aus, an denen Sie sie tangential " +"zueinander anpassen möchten." + +#: constraint.cpp:244 +msgid "" +"The curves must share an endpoint. Constrain them with Constrain -> On Point " +"before constraining tangent.\n" +"\n" +"Alternatively select the end points of both curves at which you want the " +"curves to be tangent." +msgstr "" +"Die Kurven müssen einen gemeinsamen Endpunkt haben. Schränken Sie mit " +"\"Einschränkung / Auf Punkt\" ein, bevor Sie die Tangente einschränken.\n" +"\n" +"Alternativ können Sie die Endpunkte beider Kurven auswählen, an denen Sie " +"sie tangential zueinander anpassen möchten." + +#: constraint.cpp:303 +msgid "" +"Bad selection for distance / diameter constraint. This constraint can apply " +"to:\n" +"\n" +" * two points (distance between points)\n" +" * a line segment (length)\n" +" * two points and a line segment or normal (projected distance)\n" +" * a workplane and a point (minimum distance)\n" +" * a line segment and a point (minimum distance)\n" +" * a plane face and a point (minimum distance)\n" +" * a circle or an arc (diameter)\n" +msgstr "" +"Ungültige Auswahl für Einschränkung Abstand / Durchmesser Diese " +"Einschränkung ist anwendbar auf:\n" +"\n" +" * zwei Punkte [Abstand zwischen Punkten]\n" +" * ein Liniensegment [Länge]\n" +" * zwei Punkte und ein Liniensegment oder Normale [projizierter Abstand]\n" +" * eine Arbeitsebene und ein Punkt [minimaler Abstand] \n" +" * ein Liniensegment und ein Punkt [minimaler Abstand]\n" +" * eine Seitenfläche und ein Punkt [minimaler Abstand]\n" +" * ein Kreis oder ein Bogen [Durchmesser]\n" + +#: constraint.cpp:366 +msgid "" +"Bad selection for on point / curve / plane constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points (points coincident)\n" +" * a point and a workplane (point in plane)\n" +" * a point and a line segment (point on line)\n" +" * a point and a circle or arc (point on curve)\n" +" * a point and one to three plane faces (point on face(s))\n" +msgstr "" +"Ungültige Auswahl für Einschränkung \"Auf Punkt / Kurve / Ebene\". Diese " +"Einschränkung ist anwendbar auf:\n" +"\n" +" * zwei oder mehr Punkte [deckungsgleich]\n" +" * einen Punkt und eine Arbeitsebene [Punkt auf Ebene]\n" +" * einen Punkt und ein Liniensegment [Punkt auf Linie]\n" +" * einen Punkt und einen Kreis oder Bogen [Punkt auf Kurve]\n" +" * einen Punkt und ein bis drei Seitenflächen [Punkt auf Fläche]\n" + +#: constraint.cpp:427 +msgid "" +"Bad selection for equal length / radius constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more line segments (equal length)\n" +" * two line segments and two points (equal point-line distances)\n" +" * a line segment and two points (equal point-line distances)\n" +" * a line segment, and a point and line segment (point-line distance " +"equals length)\n" +" * two or more circles or arcs (equal radius)\n" +" * a line segment and an arc (line segment length equals arc length)\n" +msgstr "" +"Ungültige Auswahl für Einschränkung \"gleicher Abstand / Radius\". Diese " +"Einschränkung ist anwendbar auf:\n" +"\n" +" * zwei oder mehr Liniensegmente [gleiche Länge]\n" +" * zwei Liniensegmente und zwei Punkte [gleiche Punkt-Linien-Abstände]\n" +" * ein Liniensegment und zwei Punkte [gleiche Punkt-Linien-Abstände]\n" +" * ein Liniensegment und ein Punkt oder Liniensegment [Abstand Punkt-" +"Linie gleich Länge]\n" +" * zwei oder mehr Kreise oder Bögen [gleicher Radius]\n" +" * ein Liniensegment und ein Bogen [Länge des Liniensegments gleich " +"Bogenlänge]\n" + +#: constraint.cpp:480 +msgid "" +"Bad selection for length ratio constraint. This constraint can apply to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"Ungültige Auswahl für Einschränkung \"Längenverhältnis\". Diese " +"Einschränkung ist anwendbar auf:\n" +"\n" +" * zwei Liniensegmente\n" +" * zwei Bögen\n" +" * einen Bogen und ein Liniensegment\n" + +#: constraint.cpp:515 +msgid "" +"Bad selection for length difference constraint. This constraint can apply " +"to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"Ungültige Auswahl für Einschränkung \"Längendifferenz\". Diese Einschränkung " +"ist anwendbar auf:\n" +"\n" +" * zwei Liniensegmente\n" +" * zwei Bögen\n" +" * einen Bogen und ein Liniensegment\n" + +#: constraint.cpp:550 +msgid "" +"Bad selection for at midpoint constraint. This constraint can apply to:\n" +"\n" +" * a line segment and a point (point at midpoint)\n" +" * a line segment and a workplane (line's midpoint on plane)\n" +msgstr "" +"Ungültige Auswahl für Einschränkung \"Mittelpunkt\". Diese Einschränkung ist " +"anwendbar auf:\n" +"\n" +" * ein Liniensegment und ein Punkt [Punkt auf Mittelpunkt]\n" +" * ein Liniensegment und eine Arbeitsebene [Mittelpunkt der Linie auf " +"Ebene]\n" + +#: constraint.cpp:608 +msgid "" +"Bad selection for symmetric constraint. This constraint can apply to:\n" +"\n" +" * two points or a line segment (symmetric about workplane's coordinate " +"axis)\n" +" * line segment, and two points or a line segment (symmetric about line " +"segment)\n" +" * workplane, and two points or a line segment (symmetric about " +"workplane)\n" +msgstr "" +"Ungültige Auswahl für Einschränkung \"Symmetrisch\". Diese Einschränkung ist " +"anwendbar auf:\n" +"\n" +" * zwei Punkte oder ein Liniensegment [symmetrisch zu Koordinatenachse " +"der Arbeitsebene]\n" +" * ein Liniensegment und zwei Punkte oder ein Liniensegment [symmetrisch " +"zu Liniensegment]\n" +" * eine Arbeitsebene und zwei Punkte oder ein Liniensegment [symmetrisch " +"zu Arbeitsebene]\n" + +#: constraint.cpp:623 +msgid "" +"A workplane must be active when constraining symmetric without an explicit " +"symmetry plane." +msgstr "" +"Eine Arbeitsebene muss aktiv sein, um die Symmetrie ohne explizite " +"Symmetrieebene einzuschränken." + +#: constraint.cpp:663 +msgid "" +"Activate a workplane (with Sketch -> In Workplane) before applying a " +"horizontal or vertical constraint." +msgstr "" +"Aktivieren Sie eine Arbeitsebene (mit Skizze -> In Arbeitsebene), bevor Sie " +"horizontal oder vertikal einschränken." + +#: constraint.cpp:679 +msgid "" +"Bad selection for horizontal / vertical constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points\n" +" * one or more line segments\n" +msgstr "" +"Ungültige Auswahl für Einschränkung \"horizontal / vertikal\". Diese " +"Einschränkung ist anwendbar auf:\n" +"\n" +" * zwei oder mehr Punkte\n" +" * ein oder mehr Liniensegmente\n" + +#: constraint.cpp:697 +msgid "" +"Bad selection for same orientation constraint. This constraint can apply " +"to:\n" +"\n" +" * two normals\n" +msgstr "" +"Ungültige Auswahl für Einschränkung \"gleiche Orientierung\". Diese " +"Einschränkung ist anwendbar auf:\n" +"\n" +" * zwei Normale\n" + +#: constraint.cpp:748 +msgid "Must select an angle constraint." +msgstr "Sie müssen einen eingeschränkten Winkel auswählen." + +#: constraint.cpp:761 +msgid "Must select a constraint with associated label." +msgstr "Sie müssen eine Einschränkung mit zugeordneter Kennzeichnung angeben." + +#: constraint.cpp:784 +msgid "" +"Bad selection for angle constraint. This constraint can apply to:\n" +"\n" +"Angle between:\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +"\n" +"Equal angles:\n" +" * four line segments or normals (equal angle between A,B and C,D)\n" +" * three line segments or normals (equal angle between A,B and B,C)\n" +msgstr "" +"Ungültige Auswahl für Einschränkung \"Winkel\". Diese Einschränkung ist " +"anwendbar auf:\n" +"\n" +"Winkel zwischen:\n" +" * zwei Liniensegmente\n" +" * ein Liniensegment und eine Normale\n" +" * zwei Normale\n" +"\n" +"Gleicher Winkel:\n" +" * vier Liniensegmente oder Normale [gleicher Winkel zwischen A,B und C," +"D]\n" +" * drei Liniensegmente oder Normale [gleicher Winkel zwischen A,B und B," +"C]\n" + +#: constraint.cpp:872 +msgid "Curve-curve tangency must apply in workplane." +msgstr "" +"Die Kurven-Kurven-Tangente muss in der Arbeitsebene eingeschränkt werden." + +#: constraint.cpp:887 +msgid "" +"Bad selection for parallel / tangent constraint. This constraint can apply " +"to:\n" +"\n" +" * two faces\n" +" * two or more line segments (parallel)\n" +" * one or more line segments and one or more normals (parallel)\n" +" * two or more normals (parallel)\n" +" * two line segments, arcs, or beziers, that share an endpoint (tangent)\n" +" * two line segments, arcs, or beziers, that do not share an endpoint and " +"the end point(s) of the curve(s) (tangent)\n" +msgstr "" +"Ungültige Auswahl für Einschränkung \"Parallel / Tangente\". Diese " +"Einschränkung ist anwendbar auf:\n" +"\n" +" * zwei Flächen\n" +" * zwei oder mehr Liniensegmente [parallel]\n" +" * ein oder mehr Liniensegmente und eine oder mehr Normalen [parallel]\n" +" * zwei oder mehr Normalen [parallel]\n" +" * zwei Liniensegmente, Bögen oder Beziers mit gemeinsamem Endpunkt " +"[Tangente]\n" +" * zwei Liniensegmente, Bögen oder Beziers ohne gemeinsamen Endpunkt und " +"die Endpunkte der Kurve(n) [Tangente]\n" + +#: constraint.cpp:914 +msgid "" +"Bad selection for perpendicular constraint. This constraint can apply to:\n" +"\n" +" * two faces\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +msgstr "" +"Ungültige Auswahl für Einschränkung \"Rechtwinklig\". Diese Einschränkung " +"ist anwendbar auf:\n" +"\n" +" * zwei Flächen\n" +" * zwei Liniensegmente\n" +" * ein Liniensegment und eine Normale\n" +" * zwei Normale\n" + +#: constraint.cpp:931 +msgid "" +"Bad selection for lock point where dragged constraint. This constraint can " +"apply to:\n" +"\n" +" * a point\n" +msgstr "" +"Ungültige Auswahl für Einschränkung \"Punkt an Position\". Diese " +"Einschränkung ist anwendbar auf:\n" +"\n" +" * einen Punkt\n" + +#: constraint.cpp:946 mouse.cpp:1160 +msgid "NEW COMMENT -- DOUBLE-CLICK TO EDIT" +msgstr "NEUER KOMMENTAR -- DOPPELKLICKEN ZUM BEARBEITEN" + +#: constraint.cpp:952 +msgid "click center of comment text" +msgstr "Klicken Sie auf die Mitte des Kommentartextes" + +#: export.cpp:19 +msgid "" +"No solid model present; draw one with extrudes and revolves, or use Export " +"2d View to export bare lines and curves." +msgstr "" +"Kein Festkörper vorhanden; zeichnen Sie eines mit Extrusionen und Drehungen, " +"oder exportieren Sie bloße Linien und Kurven mit \"2D-Ansicht exportieren\"." + +#: export.cpp:61 +msgid "" +"Bad selection for export section. Please select:\n" +"\n" +" * nothing, with an active workplane (workplane is section plane)\n" +" * a face (section plane through face)\n" +" * a point and two line segments (plane through point and parallel to " +"lines)\n" +msgstr "" +"Ungültige Auswahl für teilweisen Export. Bitte wählen Sie aus:\n" +"\n" +" * nichts, mit einer aktiven Arbeitsebene [Arbeitsebene ist " +"Schnittebene]\n" +" * eine Seitenfläche [Schnittebene durch Seitenfläche]\n" +" * einen Punkt und zwei Liniensegmente [Schnittebene durch Punkt und " +"parallel zu Linien]\n" + +#: export.cpp:818 +msgid "Active group mesh is empty; nothing to export." +msgstr "Das Netz der aktiven Gruppe ist leer; es gibt nichts zu exportieren." + +#: exportvector.cpp:336 +msgid "freehand lines were replaced with continuous lines" +msgstr "Freihandlinien wurden mit durchgehenden Linien ersetzt" + +#: exportvector.cpp:338 +msgid "zigzag lines were replaced with continuous lines" +msgstr "Zickzacklinien wurden mit durchgehenden Linien ersetzt" + +#: exportvector.cpp:592 +msgid "" +"Some aspects of the drawing have no DXF equivalent and were not exported:\n" +msgstr "" +"Teile der Zeichnung haben keine Entsprechung in DXF und wurden nicht " +"exportiert:\n" + +#: exportvector.cpp:838 +msgid "" +"PDF page size exceeds 200 by 200 inches; many viewers may reject this file." +msgstr "" +"Die PDF-Seitengröße überschreitet 200 x 200 Zoll; die Datei kann u.U. in " +"vielen Programmen nicht geöffnet werden." + +#: file.cpp:44 group.cpp:91 +msgctxt "group-name" +msgid "sketch-in-plane" +msgstr "Skizze-in-Ebene" + +#: file.cpp:62 +msgctxt "group-name" +msgid "#references" +msgstr "#Referenzen" + +#: file.cpp:555 +msgid "The file is empty. It may be corrupt." +msgstr "Die Datei ist leer. Es kann beschädigt sein." + +#: file.cpp:560 +msgid "" +"Unrecognized data in file. This file may be corrupt, or from a newer version " +"of the program." +msgstr "" +"Nicht erkannte Daten in der Datei. Diese Datei könnte beschädigt sein oder " +"von einer neueren Version des Programms stammen." + +#: file.cpp:876 +msgctxt "title" +msgid "Missing File" +msgstr "Fehlende Datei" + +#: file.cpp:877 +#, c-format +msgctxt "dialog" +msgid "The linked file “%s” is not present." +msgstr "Die verlinkte Datei “%s” fehlt." + +#: file.cpp:879 +msgctxt "dialog" +msgid "" +"Do you want to locate it manually?\n" +"\n" +"If you decline, any geometry that depends on the missing file will be " +"permanently removed." +msgstr "" +"Möchten Sie sie selber auswählen?\n" +"Falls Sie ablehnen, wird jegliche mit der fehlenden Datei verknüpfte " +"Geometrie verworfen." + +#: file.cpp:882 +msgctxt "button" +msgid "&Yes" +msgstr "&Ja" + +#: file.cpp:884 +msgctxt "button" +msgid "&No" +msgstr "&Nein" + +#: file.cpp:886 solvespace.cpp:652 +msgctxt "button" +msgid "&Cancel" +msgstr "&Abbrechen" + +#: graphicswin.cpp:41 +msgid "&File" +msgstr "&Datei" + +#: graphicswin.cpp:42 +msgid "&New" +msgstr "&Neu" + +#: graphicswin.cpp:43 +msgid "&Open..." +msgstr "&Öffnen..." + +#: graphicswin.cpp:44 +msgid "Open &Recent" +msgstr "Öffne &letzte" + +#: graphicswin.cpp:45 +msgid "&Save" +msgstr "&Speichern" + +#: graphicswin.cpp:46 +msgid "Save &As..." +msgstr "Speichern &Als…" + +#: graphicswin.cpp:48 +msgid "Export &Image..." +msgstr "Exportiere &Bild…" + +#: graphicswin.cpp:49 +msgid "Export 2d &View..." +msgstr "Exportiere 2D-Ansicht…" + +#: graphicswin.cpp:50 +msgid "Export 2d &Section..." +msgstr "Exportiere 2D-Auswahl…" + +#: graphicswin.cpp:51 +msgid "Export 3d &Wireframe..." +msgstr "Exportiere 3D-Drahtgittermodell..." + +#: graphicswin.cpp:52 +msgid "Export Triangle &Mesh..." +msgstr "Exportiere Dreiecksnetz…" + +#: graphicswin.cpp:53 +msgid "Export &Surfaces..." +msgstr "Exportiere Oberflächen…" + +#: graphicswin.cpp:54 +msgid "Im&port..." +msgstr "Im&portiere…" + +#: graphicswin.cpp:57 +msgid "E&xit" +msgstr "Beenden" + +#: graphicswin.cpp:60 +msgid "&Edit" +msgstr "&Bearbeiten" + +#: graphicswin.cpp:61 +msgid "&Undo" +msgstr "&Rückgängig machen" + +#: graphicswin.cpp:62 +msgid "&Redo" +msgstr "&Wiederholen" + +#: graphicswin.cpp:63 +msgid "Re&generate All" +msgstr "Alles &neu zeichnen" + +#: graphicswin.cpp:65 +msgid "Snap Selection to &Grid" +msgstr "Auswahl auf &Raster ausrichten" + +#: graphicswin.cpp:66 +msgid "Rotate Imported &90°" +msgstr "Importierte Objekte &90° drehen" + +#: graphicswin.cpp:68 +msgid "Cu&t" +msgstr "Ausschneiden" + +#: graphicswin.cpp:69 +msgid "&Copy" +msgstr "Kopieren" + +#: graphicswin.cpp:70 +msgid "&Paste" +msgstr "Einfügen" + +#: graphicswin.cpp:71 +msgid "Paste &Transformed..." +msgstr "&Transformiert einfügen…" + +#: graphicswin.cpp:72 +msgid "&Delete" +msgstr "&Löschen" + +#: graphicswin.cpp:74 +msgid "Select &Edge Chain" +msgstr "&Kantenverlauf auswählen" + +#: graphicswin.cpp:75 +msgid "Select &All" +msgstr "&Alle auswählen" + +#: graphicswin.cpp:76 +msgid "&Unselect All" +msgstr "Alle &deselektieren" + +#: graphicswin.cpp:78 +msgid "&Line Styles..." +msgstr "&Linien Stile..." + +#: graphicswin.cpp:79 +msgid "&View Projection..." +msgstr "An&sichts Projektion..." + +#: graphicswin.cpp:81 +msgid "Con&figuration..." +msgstr "&Einstellungen..." + +#: graphicswin.cpp:84 +msgid "&View" +msgstr "Ansicht" + +#: graphicswin.cpp:85 +msgid "Zoom &In" +msgstr "Zoom größer" + +#: graphicswin.cpp:86 +msgid "Zoom &Out" +msgstr "Zoom kleiner" + +#: graphicswin.cpp:87 +msgid "Zoom To &Fit" +msgstr "Zoom anpassen" + +#: graphicswin.cpp:89 +msgid "Align View to &Workplane" +msgstr "Ansicht auf Arbeitsebene ausrichten" + +#: graphicswin.cpp:90 +msgid "Nearest &Ortho View" +msgstr "Nächste Ortho-Ansicht" + +#: graphicswin.cpp:91 +msgid "Nearest &Isometric View" +msgstr "Nächste isometrische Ansicht" + +#: graphicswin.cpp:92 +msgid "&Center View At Point" +msgstr "Ansicht auf Punkt zentrieren" + +#: graphicswin.cpp:94 +msgid "Show Snap &Grid" +msgstr "Arbeitsraster anzeigen" + +#: graphicswin.cpp:95 +msgid "Darken Inactive Solids" +msgstr "Dunklere inaktive Festkörper" + +#: graphicswin.cpp:96 +msgid "Use &Perspective Projection" +msgstr "Perspektivische Projektion" + +#: graphicswin.cpp:97 +msgid "Show E&xploded View" +msgstr "Zeige e&xplodierte Ansicht" + +#: graphicswin.cpp:98 +msgid "Dimension &Units" +msgstr "Maßeinheit" + +#: graphicswin.cpp:99 +msgid "Dimensions in &Millimeters" +msgstr "Maße in Millimeter" + +#: graphicswin.cpp:100 +msgid "Dimensions in M&eters" +msgstr "Masse in M&etern" + +#: graphicswin.cpp:101 +msgid "Dimensions in &Inches" +msgstr "Maße in Zoll" + +#: graphicswin.cpp:102 +msgid "Dimensions in &Feet and Inches" +msgstr "Maße in &Fuß und Inch" + +#: graphicswin.cpp:104 +msgid "Show &Toolbar" +msgstr "Werkzeugleiste anzeigen" + +#: graphicswin.cpp:105 +msgid "Show Property Bro&wser" +msgstr "Attributbrowser anzeigen" + +#: graphicswin.cpp:107 +msgid "&Full Screen" +msgstr "Vollbildschirm" + +#: graphicswin.cpp:109 +msgid "&New Group" +msgstr "Neue Gruppe" + +#: graphicswin.cpp:110 +msgid "Sketch In &3d" +msgstr "In 3D skizzieren" + +#: graphicswin.cpp:111 +msgid "Sketch In New &Workplane" +msgstr "In neuer Arbeitsebene skizzieren" + +#: graphicswin.cpp:113 +msgid "Step &Translating" +msgstr "Kopieren und verschieben" + +#: graphicswin.cpp:114 +msgid "Step &Rotating" +msgstr "Kopieren und drehen" + +#: graphicswin.cpp:116 +msgid "E&xtrude" +msgstr "E&xtrudieren" + +#: graphicswin.cpp:117 +msgid "&Helix" +msgstr "&Helix" + +#: graphicswin.cpp:118 +msgid "&Lathe" +msgstr "R&otieren" + +#: graphicswin.cpp:119 +msgid "Re&volve" +msgstr "D&rehen" + +#: graphicswin.cpp:121 +msgid "Link / Assemble..." +msgstr "Verknüpfen / Zusammensetzen..." + +#: graphicswin.cpp:122 +msgid "Link Recent" +msgstr "Letzte verknüpfen" + +#: graphicswin.cpp:124 +msgid "&Sketch" +msgstr "&Skizze" + +#: graphicswin.cpp:125 +msgid "In &Workplane" +msgstr "In Arbeitsebene" + +#: graphicswin.cpp:126 +msgid "Anywhere In &3d" +msgstr "Im 3D-Raum" + +#: graphicswin.cpp:128 +msgid "Datum &Point" +msgstr "Bezugspunkt" + +#: graphicswin.cpp:129 +msgid "Wor&kplane" +msgstr "Arbeits&ebene" + +#: graphicswin.cpp:131 +msgid "Line &Segment" +msgstr "&Linie" + +#: graphicswin.cpp:132 +msgid "C&onstruction Line Segment" +msgstr "K&onstruktionslinie" + +#: graphicswin.cpp:133 +msgid "&Rectangle" +msgstr "&Rechteck" + +#: graphicswin.cpp:134 +msgid "&Circle" +msgstr "&Kreis" + +#: graphicswin.cpp:135 +msgid "&Arc of a Circle" +msgstr "Kreisbogen" + +#: graphicswin.cpp:136 +msgid "&Bezier Cubic Spline" +msgstr "Kubischer &Bezier-Spline" + +#: graphicswin.cpp:138 +msgid "&Text in TrueType Font" +msgstr "&Text in Truetype-Font" + +#: graphicswin.cpp:139 +msgid "I&mage" +msgstr "B&ild" + +#: graphicswin.cpp:141 +msgid "To&ggle Construction" +msgstr "Konstruktionselement an/aus" + +#: graphicswin.cpp:142 +msgid "Ta&ngent Arc at Point" +msgstr "Bogentangente an Punkt" + +#: graphicswin.cpp:143 +msgid "Split Curves at &Intersection" +msgstr "Kurven im Schnittpunkt trennen" + +#: graphicswin.cpp:145 +msgid "&Constrain" +msgstr "&Einschränkung" + +#: graphicswin.cpp:146 +msgid "&Distance / Diameter" +msgstr "Abstand / Durchmesser" + +#: graphicswin.cpp:147 +msgid "Re&ference Dimension" +msgstr "Referenzangabe" + +#: graphicswin.cpp:148 +msgid "A&ngle / Equal Angle" +msgstr "Winkel / Gleicher Winkel" + +#: graphicswin.cpp:149 +msgid "Reference An&gle" +msgstr "Referenzwinkel" + +#: graphicswin.cpp:150 +msgid "Other S&upplementary Angle" +msgstr "Komplementärwinkel" + +#: graphicswin.cpp:151 +msgid "Toggle R&eference Dim" +msgstr "Referenzangabe ein/aus" + +#: graphicswin.cpp:153 +msgid "&Horizontal" +msgstr "Horizontal" + +#: graphicswin.cpp:154 +msgid "&Vertical" +msgstr "&Vertikal" + +#: graphicswin.cpp:156 +msgid "&On Point / Curve / Plane" +msgstr "Auf Punkt / Kurve / Ebene" + +#: graphicswin.cpp:157 +msgid "E&qual Length / Radius" +msgstr "Gleicher Abstand / Radius" + +#: graphicswin.cpp:158 +msgid "Length / Arc Ra&tio" +msgstr "Länge / Bogen Verhäl&tnis" + +#: graphicswin.cpp:159 +msgid "Length / Arc Diff&erence" +msgstr "Länge / Bogen Diff&erenz" + +#: graphicswin.cpp:160 +msgid "At &Midpoint" +msgstr "Auf &Mittelpunkt" + +#: graphicswin.cpp:161 +msgid "S&ymmetric" +msgstr "Symmetrisch" + +#: graphicswin.cpp:162 +msgid "Para&llel / Tangent" +msgstr "Paral&llel / Tangente" + +#: graphicswin.cpp:163 +msgid "&Perpendicular" +msgstr "Rechtwinklig" + +#: graphicswin.cpp:164 +msgid "Same Orient&ation" +msgstr "Gleiche Orientierung" + +#: graphicswin.cpp:165 +msgid "Lock Point Where &Dragged" +msgstr "Punkt an Position fixieren" + +#: graphicswin.cpp:167 +msgid "Comment" +msgstr "Kommentar" + +#: graphicswin.cpp:169 +msgid "&Analyze" +msgstr "&Analyse" + +#: graphicswin.cpp:170 +msgid "Measure &Volume" +msgstr "&Volumen bestimmen" + +#: graphicswin.cpp:171 +msgid "Measure A&rea" +msgstr "Fläche bestimmen" + +#: graphicswin.cpp:172 +msgid "Measure &Perimeter" +msgstr "Umfang bestimmen" + +#: graphicswin.cpp:173 +msgid "Show &Interfering Parts" +msgstr "Überlagernde Teile anzeigen" + +#: graphicswin.cpp:174 +msgid "Show &Naked Edges" +msgstr "Freiliegende Kanten anzeigen" + +#: graphicswin.cpp:175 +msgid "Show &Center of Mass" +msgstr "Massenmittelpunkt anzeigen" + +#: graphicswin.cpp:177 +msgid "Show &Underconstrained Points" +msgstr "&Unterbeschränkte Punkte anzeigen" + +#: graphicswin.cpp:179 +msgid "&Trace Point" +msgstr "Punkt nachzeichnen" + +#: graphicswin.cpp:180 +msgid "&Stop Tracing..." +msgstr "Nachzeichnen beenden..." + +#: graphicswin.cpp:181 +msgid "Step &Dimension..." +msgstr "Schrittgröße…" + +#: graphicswin.cpp:183 +msgid "&Help" +msgstr "&Hilfe" + +#: graphicswin.cpp:184 +msgid "&Language" +msgstr "Sprache" + +#: graphicswin.cpp:185 +msgid "&Website / Manual" +msgstr "&Website / Anleitung" + +#: graphicswin.cpp:186 +msgid "&Go to GitHub commit" +msgstr "&Gehe zu GitHub commit" + +#: graphicswin.cpp:188 +msgid "&About" +msgstr "Über" + +#: graphicswin.cpp:362 +msgid "(no recent files)" +msgstr "(keine vorhergehenden Dateien)" + +#: graphicswin.cpp:370 +#, c-format +msgid "File '%s' does not exist." +msgstr "Datei '%s' existiert nicht." + +#: graphicswin.cpp:779 +msgid "No workplane is active, so the grid will not appear." +msgstr "" +"Das Raster wird nicht angezeigt, weil keine Arbeitsebene ausgewählt ist." + +#: graphicswin.cpp:794 +msgid "" +"The perspective factor is set to zero, so the view will always be a parallel " +"projection.\n" +"\n" +"For a perspective projection, modify the perspective factor in the " +"configuration screen. A value around 0.3 is typical." +msgstr "" +"Der Perspektivfaktor is auf Null gesetzt, also wird die Ansicht eine " +"Parallelprojektion sein.\n" +"\n" +"Ändern Sie den Faktor für die Perspektivprojektion in der " +"Konfigurationsmaske. Ein typischer Wert ist ca. 0,3." + +#: graphicswin.cpp:884 +msgid "" +"Select a point; this point will become the center of the view on screen." +msgstr "" +"Wählen Sie einen Punkt aus; dieser Punkt wird im Mittelpunkt der " +"Bildschirmansicht sein." + +#: graphicswin.cpp:1193 +msgid "No additional entities share endpoints with the selected entities." +msgstr "" +"Die ausgewählten Objekte teilen keine gemeinsamen Endpunkte mit anderen " +"Objekten." + +#: graphicswin.cpp:1211 +msgid "" +"To use this command, select a point or other entity from an linked part, or " +"make a link group the active group." +msgstr "" +"Für diesen Befehl wählen Sie einen Punkt oder ein anderes Objekt von einem " +"verknüpften Teil aus, oder aktivieren Sie eine verknüpfte Gruppe." + +#: graphicswin.cpp:1234 +msgid "" +"No workplane is active. Activate a workplane (with Sketch -> In Workplane) " +"to define the plane for the snap grid." +msgstr "" +"Es wurde keine Arbeitsebene ausgewählt. Aktivieren Sie eine Arbeitsebene " +"(mit Skizze -> In Arbeitsebene), um die Ebene für das Gitterraster zu " +"definieren." + +#: graphicswin.cpp:1241 +msgid "" +"Can't snap these items to grid; select points, text comments, or constraints " +"with a label. To snap a line, select its endpoints." +msgstr "" +"Diese Objekte können nicht auf das Raster ausgerichtet werden. Dies geht nur " +"für Punkte, Textkommentare, oder Einschränkungen mit einer Bezeichnung. Um " +"eine Linie auf das Raster auszurichten, wählen Sie deren Endpunkte aus." + +#: graphicswin.cpp:1326 +msgid "No workplane selected. Activating default workplane for this group." +msgstr "" +"Es wurde keine Arbeitsebene ausgewählt. Die Standard-Arbeitsebene für diese " +"Gruppe wird aktiviert." + +#: graphicswin.cpp:1329 +msgid "" +"No workplane is selected, and the active group does not have a default " +"workplane. Try selecting a workplane, or activating a sketch-in-new-" +"workplane group." +msgstr "" +"Es wurde keine Arbeitsebene ausgewählt, und die aktive Gruppe hat keine " +"standardmäßige Arbeitsebene. Wählen Sie eine Arbeitsebene aus, oder " +"erstellen Sie eine Gruppe in einer neuen Arbeitsebene." + +#: graphicswin.cpp:1350 +msgid "" +"Bad selection for tangent arc at point. Select a single point, or select " +"nothing to set up arc parameters." +msgstr "" +"Ungültige Auswahl für Bogentangente an Punkt. Wählen Sie einen einzelnen " +"Punkt. Um die Bogenparameter anzugeben, wählen Sie nichts aus." + +#: graphicswin.cpp:1361 +msgid "click point on arc (draws anti-clockwise)" +msgstr "" +"Erstellen Sie einen Punkt auf dem Bogen (zeichnet im Gegenuhrzeigersinn)" + +#: graphicswin.cpp:1362 +msgid "click to place datum point" +msgstr "Klicken Sie, um einen Bezugspunkt zu platzieren" + +#: graphicswin.cpp:1363 +msgid "click first point of line segment" +msgstr "Klicken Sie auf den ersten Punkt des Liniensegments" + +#: graphicswin.cpp:1365 +msgid "click first point of construction line segment" +msgstr "Klicken Sie auf den ersten Punkt der Konstruktionslinie" + +#: graphicswin.cpp:1366 +msgid "click first point of cubic segment" +msgstr "Klicken Sie auf den ersten Punkt der kubischen Linie" + +#: graphicswin.cpp:1367 +msgid "click center of circle" +msgstr "Klicken Sie auf den Kreismittelpunkt" + +#: graphicswin.cpp:1368 +msgid "click origin of workplane" +msgstr "Klicken Sie auf den Ursprungspunkt der Arbeitsebene" + +#: graphicswin.cpp:1369 +msgid "click one corner of rectangle" +msgstr "Klicken Sie auf eine Ecke des Rechtecks" + +#: graphicswin.cpp:1370 +msgid "click top left of text" +msgstr "Klicken Sie auf die obere linke Ecke des Texts" + +#: graphicswin.cpp:1376 +msgid "click top left of image" +msgstr "Klicken Sie auf die obere linke Ecke des Bilds" + +#: graphicswin.cpp:1402 +msgid "" +"No entities are selected. Select entities before trying to toggle their " +"construction state." +msgstr "" +"Es wurden keine Objekte ausgewählt Wählen Sie Objekte aus, bevor Sie sie von/" +"zu Konstruktionsmerkmalen umwandeln." + +#: group.cpp:86 +msgctxt "group-name" +msgid "sketch-in-3d" +msgstr "Skizze-in-3D" + +#: group.cpp:154 +msgid "" +"Bad selection for new sketch in workplane. This group can be created with:\n" +"\n" +" * a point (through the point, orthogonal to coordinate axes)\n" +" * a point and two line segments (through the point, parallel to the " +"lines)\n" +" * a point and a normal (through the point, orthogonal to the normal)\n" +" * a workplane (copy of the workplane)\n" +msgstr "" +"Ungültige Auswahl für neue Skizze in der Arbeitsebene. Diese Gruppe kann " +"erstellt werden mit:\n" +"\n" +" * einem Punkt (durch den Punkt, orthogonal zur Koordinatenachse)\n" +" * einem Punkt und zwei Linienabschnitten (durch den Punkt, parallel zu " +"den Linien)\n" + +#: group.cpp:170 +msgid "" +"Activate a workplane (Sketch -> In Workplane) before extruding. The sketch " +"will be extruded normal to the workplane." +msgstr "" +"Aktivieren Sie vor der Extrusion eine Arbeitsebene (mit Skizze -> In " +"Arbeitsebene). Die Skizze wird senkrecht zur Arbeitsebene extrudiert." + +#: group.cpp:179 +msgctxt "group-name" +msgid "extrude" +msgstr "Extrusion" + +#: group.cpp:184 +msgid "Lathe operation can only be applied to planar sketches." +msgstr "Rotieren kann nur mit planaren Skizzen ausgeführt werden." + +#: group.cpp:195 +msgid "" +"Bad selection for new lathe group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Ungültige Auswahl für neue Gruppe mit Drehquerschnitt. Diese Gruppe kann " +"erstellt werden mit:\n" +"\n" +" * einem Punkt und einem Liniensegment oder einer Normale (Drehung um " +"eine Achse parallel zur Linie/Normalen, durch den Punkt)\n" +" * einem Liniensegment (Drehung um das Liniensegment)\n" + +#: group.cpp:205 +msgctxt "group-name" +msgid "lathe" +msgstr "Drehung" + +#: group.cpp:210 +msgid "Revolve operation can only be applied to planar sketches." +msgstr "Revolve kann nur mit planaren Skizzen ausgeführt werden." + +#: group.cpp:221 +msgid "" +"Bad selection for new revolve group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Ungültige Auswahl für eine neue Revolve Gruppe. Diese Gruppe kann erzeugt " +"werden mit:\n" +"\n" +" * einem Punkt und einem Liniensegment oder Normale (gedreht um eine " +"Achse parallel zu Linie / Normale, durch Punkt)\n" +" * einem Liniensegment (gedreht um Liniensegment)\n" + +#: group.cpp:233 +msgctxt "group-name" +msgid "revolve" +msgstr "Revolve" + +#: group.cpp:238 +msgid "Helix operation can only be applied to planar sketches." +msgstr "Helix kann nur mit planaren Skizzen ausgeführt werden." + +#: group.cpp:249 +msgid "" +"Bad selection for new helix group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Ungültige Auswahl für eine neue Helix Gruppe. Diese Gruppe kann erzeugt " +"werden mit:\n" +"\n" +" * einem Punkt und einem Liniensegment oder Normale (gedreht um eine " +"Achse parallel zu Linie / Normale, durch Punkt)\n" +" * einem Liniensegment (gedreht um Liniensegment)\n" + +#: group.cpp:261 +msgctxt "group-name" +msgid "helix" +msgstr "Helix" + +#: group.cpp:274 +msgid "" +"Bad selection for new rotation. This group can be created with:\n" +"\n" +" * a point, while locked in workplane (rotate in plane, about that " +"point)\n" +" * a point and a line or a normal (rotate about an axis through the " +"point, and parallel to line / normal)\n" +msgstr "" +"Ungültige Auswahl für neue Rotation. Diese Gruppe kann erstellt werden mit:\n" +"\n" +" * einem Punkt in vorgegebener Arbeitsebene (gedreht in der Ebene um den " +"Punkt)\n" +" * einem Punkt und einer Linie oder einer Normale (gedreht um eine Achse " +"durch den Punkt, parallel zur Linie / Normale)\n" + +#: group.cpp:287 +msgctxt "group-name" +msgid "rotate" +msgstr "Drehen" + +#: group.cpp:298 +msgctxt "group-name" +msgid "translate" +msgstr "Versetzen" + +#: group.cpp:422 +msgid "(unnamed)" +msgstr "unbenannt" + +#: groupmesh.cpp:710 +msgid "not closed contour, or not all same style!" +msgstr "Kontur nicht geschlossen, oder kein einheitlicher Linientyp!" + +#: groupmesh.cpp:723 +msgid "points not all coplanar!" +msgstr "Punkte sind nicht alle koplanar!" + +#: groupmesh.cpp:725 +msgid "contour is self-intersecting!" +msgstr "Kontur überschneidet sich selbst!" + +#: groupmesh.cpp:727 +msgid "zero-length edge!" +msgstr "Kante mit Länge Null!" + +#: importmesh.cpp:136 +msgid "Text-formated STL files are not currently supported" +msgstr "Text-formatierte STL Dateien werden aktuell nicht unterstützt" + +#: modify.cpp:252 +msgid "Must be sketching in workplane to create tangent arc." +msgstr "Eine Bogentangente kann nur in einer Arbeitsebene erstellt werden." + +#: modify.cpp:299 +msgid "" +"To create a tangent arc, select a point where two non-construction lines or " +"circles in this group and workplane join." +msgstr "" +"Um eine Bogentangente zu erstellen, wählen Sie einen Punkt, in dem sich zwei " +"nicht-Konstruktionslinien oder -kreise in dieser Gruppe und Arbeitsebene " +"treffen." + +#: modify.cpp:386 +msgid "" +"Couldn't round this corner. Try a smaller radius, or try creating the " +"desired geometry by hand with tangency constraints." +msgstr "" +"Diese Ecke konnte nicht abgerundet werden. Versuchen Sie einen kleineren " +"Radius, oder erstellen Sie die gewünschte Geometrie von Hand mit " +"\"Tangente\"-Einschränkungen." + +#: modify.cpp:595 +msgid "Couldn't split this entity; lines, circles, or cubics only." +msgstr "" +"Dieses Objekt konnte nicht geteilt werden. Dies geht nur für Linien, Kreise " +"oder kubische Splines." + +#: modify.cpp:622 +msgid "Must be sketching in workplane to split." +msgstr "Trennen ist nur in einer Arbeitsebene möglich." + +#: modify.cpp:629 +msgid "" +"Select two entities that intersect each other (e.g. two lines/circles/arcs " +"or a line/circle/arc and a point)." +msgstr "" +"Wählen Sie zwei Objekte aus, die sich schneiden (z.B. zwei Linien/Kreise/" +"Bögen, oder eine Linie/Kreis/Bogen und ein Punkt)." + +#: modify.cpp:734 +msgid "Can't split; no intersection found." +msgstr "Trennen nicht möglich; keine Überschneidung gefunden." + +#: mouse.cpp:558 +msgid "Assign to Style" +msgstr "Linientyp zuordnen" + +#: mouse.cpp:574 +msgid "No Style" +msgstr "Kein Linientyp" + +#: mouse.cpp:577 +msgid "Newly Created Custom Style..." +msgstr "Neu erstellter benutzerdefinierter Linientyp…" + +#: mouse.cpp:584 +msgid "Group Info" +msgstr "Info zu Gruppe" + +#: mouse.cpp:604 +msgid "Style Info" +msgstr "Info zu Linientyp" + +#: mouse.cpp:624 +msgid "Select Edge Chain" +msgstr "Kantenverlauf auswählen" + +#: mouse.cpp:630 +msgid "Toggle Reference Dimension" +msgstr "Von/zu Referenzangabe wechseln" + +#: mouse.cpp:636 +msgid "Other Supplementary Angle" +msgstr "Anderer Komplementärwinkel" + +#: mouse.cpp:641 +msgid "Snap to Grid" +msgstr "Auf Raster ausrichten" + +#: mouse.cpp:650 +msgid "Remove Spline Point" +msgstr "Spline-Punkt löschen" + +#: mouse.cpp:685 +msgid "Add Spline Point" +msgstr "Spline-Punkt hinzufügen" + +#: mouse.cpp:689 +msgid "Cannot add spline point: maximum number of points reached." +msgstr "" +"Spline-Punkt kann nicht hinzugefügt werden: maximale Anzahl der Punkte " +"erreicht." + +#: mouse.cpp:714 +msgid "Toggle Construction" +msgstr "Konstruktionselement an/aus" + +#: mouse.cpp:730 +msgid "Delete Point-Coincident Constraint" +msgstr "Einschränkung \"Punkte deckungsgleich\" löschen" + +#: mouse.cpp:748 +msgid "Cut" +msgstr "Ausschneiden" + +#: mouse.cpp:750 +msgid "Copy" +msgstr "Kopieren" + +#: mouse.cpp:754 +msgid "Select All" +msgstr "Alle auswählen" + +#: mouse.cpp:759 +msgid "Paste" +msgstr "Einfügen" + +#: mouse.cpp:761 +msgid "Paste Transformed..." +msgstr "Einfügen und transformieren…" + +#: mouse.cpp:766 +msgid "Delete" +msgstr "Löschen" + +#: mouse.cpp:769 +msgid "Unselect All" +msgstr "Alle deselektieren" + +#: mouse.cpp:776 +msgid "Unselect Hovered" +msgstr "Aktive deselektieren" + +#: mouse.cpp:785 +msgid "Zoom to Fit" +msgstr "Zoom an Bildschirm anpassen" + +#: mouse.cpp:987 mouse.cpp:1276 +msgid "click next point of line, or press Esc" +msgstr "Klicken Sie auf den nächsten Punkt der Linie, oder drücken Sie Esc" + +#: mouse.cpp:993 +msgid "" +"Can't draw rectangle in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Ein Rechteck kann nicht in 3D erstellt werden. Aktivieren Sie zuerst eine " +"Arbeitsebene mit \"Skizze -> In Arbeitsebene\"." + +#: mouse.cpp:1027 +msgid "click to place other corner of rectangle" +msgstr "Klicken Sie auf die gegenüberliegende Ecke des Rechtecks" + +#: mouse.cpp:1048 +msgid "click to set radius" +msgstr "Klicken Sie, um den Radius festzulegen" + +#: mouse.cpp:1053 +msgid "" +"Can't draw arc in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Ein Kreisbogen kann nicht in 3D erstellt werden. Aktivieren Sie zuerst eine " +"Arbeitsebene mit \"Skizze -> In Arbeitsebene\"." + +#: mouse.cpp:1072 +msgid "click to place point" +msgstr "Klicken Sie, um einen Punkt zu platzieren" + +#: mouse.cpp:1088 +msgid "click next point of cubic, or press Esc" +msgstr "" +"Klicken Sie auf den nächsten Punkt der kubischen Linie, oder drücken Sie Esc" + +#: mouse.cpp:1093 +msgid "" +"Sketching in a workplane already; sketch in 3d before creating new workplane." +msgstr "" +"Eine Arbeitsebene ist bereits aktiv. Skizzieren Sie in 3D, bevor Sie eine " +"neue Arbeitsebene erstellen." + +#: mouse.cpp:1109 +msgid "" +"Can't draw text in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Text kann nicht in 3D erstellt werden. Aktivieren Sie zuerst eine " +"Arbeitsebene mit \"Skizze -> In Arbeitsebene\"." + +#: mouse.cpp:1126 +msgid "click to place bottom right of text" +msgstr "Klicken Sie auf die untere rechte Ecke des Texts" + +#: mouse.cpp:1132 +msgid "" +"Can't draw image in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Das Bild kann nicht in 3D erstellt werden. Aktivieren Sie zuerst eine " +"Arbeitsebene mit \"Skizze -> In Arbeitsebene\"." + +#: platform/gui.cpp:85 platform/gui.cpp:90 solvespace.cpp:583 +msgctxt "file-type" +msgid "SolveSpace models" +msgstr "SolveSpace-Modelle" + +#: platform/gui.cpp:89 +msgctxt "file-type" +msgid "ALL" +msgstr "ALLE" + +#: platform/gui.cpp:91 +msgctxt "file-type" +msgid "IDF circuit board" +msgstr "IDF Leiterplatte" + +#: platform/gui.cpp:92 +msgctxt "file-type" +msgid "STL triangle mesh" +msgstr "STL-Dreiecks-Netz" + +#: platform/gui.cpp:96 +msgctxt "file-type" +msgid "PNG image" +msgstr "PNG-Datei" + +#: platform/gui.cpp:100 +msgctxt "file-type" +msgid "STL mesh" +msgstr "STL-Netz" + +#: platform/gui.cpp:101 +msgctxt "file-type" +msgid "Wavefront OBJ mesh" +msgstr "Wavefront OBJ-Netz" + +#: platform/gui.cpp:102 +msgctxt "file-type" +msgid "Three.js-compatible mesh, with viewer" +msgstr "Three.js-kompatibles Netz, mit Ansicht" + +#: platform/gui.cpp:103 +msgctxt "file-type" +msgid "Three.js-compatible mesh, mesh only" +msgstr "Three.js-kompatibles Netz, nur Netz" + +#: platform/gui.cpp:104 +msgctxt "file-type" +msgid "VRML text file" +msgstr "VRML Textdatei" + +#: platform/gui.cpp:108 platform/gui.cpp:115 platform/gui.cpp:122 +msgctxt "file-type" +msgid "STEP file" +msgstr "STEP-Datei" + +#: platform/gui.cpp:112 +msgctxt "file-type" +msgid "PDF file" +msgstr "PDF-Datei" + +#: platform/gui.cpp:113 +msgctxt "file-type" +msgid "Encapsulated PostScript" +msgstr "Eingebettetes Postscript" + +#: platform/gui.cpp:114 +msgctxt "file-type" +msgid "Scalable Vector Graphics" +msgstr "Skalierbare Vektorgrafik" + +#: platform/gui.cpp:116 platform/gui.cpp:123 +msgctxt "file-type" +msgid "DXF file (AutoCAD 2007)" +msgstr "DXF-Datei (AutoCAD 2007)" + +#: platform/gui.cpp:117 +msgctxt "file-type" +msgid "HPGL file" +msgstr "HPGL-Datei" + +#: platform/gui.cpp:118 +msgctxt "file-type" +msgid "G Code" +msgstr "G-Code" + +#: platform/gui.cpp:127 +msgctxt "file-type" +msgid "AutoCAD DXF and DWG files" +msgstr "AutoCAD DXF- und DWG-Dateien" + +#: platform/gui.cpp:131 +msgctxt "file-type" +msgid "Comma-separated values" +msgstr "Werte durch Komma getrennt (CSV)" + +#: platform/guigtk.cpp:1434 platform/guimac.mm:1513 platform/guiwin.cpp:1641 +msgid "untitled" +msgstr "unbenannt" + +#: platform/guigtk.cpp:1445 platform/guigtk.cpp:1481 platform/guimac.mm:1471 +#: platform/guiwin.cpp:1639 +msgctxt "title" +msgid "Save File" +msgstr "Datei speichern" + +#: platform/guigtk.cpp:1446 platform/guigtk.cpp:1482 platform/guimac.mm:1454 +#: platform/guiwin.cpp:1645 +msgctxt "title" +msgid "Open File" +msgstr "Datei öffnen" + +#: platform/guigtk.cpp:1449 platform/guigtk.cpp:1488 +msgctxt "button" +msgid "_Cancel" +msgstr "_Abbrechen" + +#: platform/guigtk.cpp:1450 platform/guigtk.cpp:1486 +msgctxt "button" +msgid "_Save" +msgstr "_Speichern" + +#: platform/guigtk.cpp:1451 platform/guigtk.cpp:1487 +msgctxt "button" +msgid "_Open" +msgstr "_Öffnen" + +#: solvespace.cpp:175 +msgctxt "title" +msgid "Autosave Available" +msgstr "Automatische Sicherungsdatei verfügbar" + +#: solvespace.cpp:176 +msgctxt "dialog" +msgid "An autosave file is available for this sketch." +msgstr "Eine automatische Sicherung ist für diese Skizze verfügbar." + +#: solvespace.cpp:177 +msgctxt "dialog" +msgid "Do you want to load the autosave file instead?" +msgstr "Wollen Sie die automatische Sicherungsdatei stattdessen laden?" + +#: solvespace.cpp:178 +msgctxt "button" +msgid "&Load autosave" +msgstr "AutoDatei &öffnen" + +#: solvespace.cpp:180 +msgctxt "button" +msgid "Do&n't Load" +msgstr "&Nicht laden" + +#: solvespace.cpp:640 +msgctxt "title" +msgid "Modified File" +msgstr "Geänderte Datei" + +#: solvespace.cpp:642 +#, c-format +msgctxt "dialog" +msgid "Do you want to save the changes you made to the sketch “%s”?" +msgstr "Wollen Sie die Änderungen an der Skizze “%s” sichern?" + +#: solvespace.cpp:645 +msgctxt "dialog" +msgid "Do you want to save the changes you made to the new sketch?" +msgstr "Wollen Sie die Änderungen an der Skizze sichern?" + +#: solvespace.cpp:648 +msgctxt "dialog" +msgid "Your changes will be lost if you don't save them." +msgstr "Ihre Änderungen werden verworfen, wenn sie nicht abgespeichert werden." + +#: solvespace.cpp:649 +msgctxt "button" +msgid "&Save" +msgstr "&Sichern" + +#: solvespace.cpp:651 +msgctxt "button" +msgid "Do&n't Save" +msgstr "&Verwerfen" + +# solvespace.cpp:557 +#: solvespace.cpp:672 +msgctxt "title" +msgid "(new sketch)" +msgstr "(Neue Skizze)" + +#: solvespace.cpp:683 +msgctxt "title" +msgid "Property Browser" +msgstr "Attribut-Browser" + +#: solvespace.cpp:746 +msgid "" +"Constraints are currently shown, and will be exported in the toolpath. This " +"is probably not what you want; hide them by clicking the link at the top of " +"the text window." +msgstr "" +"Einschränkungen sind momentan sichtbar und werden exportiert. Das wollen Sie " +"wahrscheinlich nicht. Verstecken Sie sie in dem auf den Link oben im " +"Textfenster klicken." + +#: solvespace.cpp:834 +#, c-format +msgid "" +"Can't identify file type from file extension of filename '%s'; try .dxf or ." +"dwg." +msgstr "" +"Kann den Dateityp der Datei '%s' nicht auf Grund der Dateierweiterung " +"erkennen. Versuchen Sie .dxf oder .dwg." + +#: solvespace.cpp:886 +msgid "Constraint must have a label, and must not be a reference dimension." +msgstr "" +"Die Einschränkung muss einen Namen haben, und darf keine " +"Referenzdimensionierung sein." + +#: solvespace.cpp:890 +msgid "Bad selection for step dimension; select a constraint." +msgstr "" +"Falsche Auswahl für die Schrittdimensionierung. Wählen Sie eine " +"Einschränkung." + +#: solvespace.cpp:914 +msgid "The assembly does not interfere, good." +msgstr "Der Zusammenbau funktioniert, gut." + +#: solvespace.cpp:930 +#, c-format +msgid "" +"The volume of the solid model is:\n" +"\n" +" %s" +msgstr "" +"Das Volumen des Modell ist:\n" +"\n" +" %s" + +#: solvespace.cpp:939 +#, c-format +msgid "" +"\n" +"The volume of current group mesh is:\n" +"\n" +" %s" +msgstr "" +"\n" +"Das Volumen des Netz der aktiven Gruppe ist:\n" +"\n" +" %s" + +#: solvespace.cpp:944 +msgid "" +"\n" +"\n" +"Curved surfaces have been approximated as triangles.\n" +"This introduces error, typically of around 1%." +msgstr "" +"\n" +"\n" +"Gekrümmte Flächen wurden als Dreiecksnetz angenähert.\n" +"Das verursacht Fehler, typischerweise um 1%." + +#: solvespace.cpp:959 +#, c-format +msgid "" +"The surface area of the selected faces is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"Die Fläche der ausgewählten Flächen ist:\n" +"\n" +" %s\n" +"\n" +"Kurven wurden als gerade Linienstücke angenähert.\n" +"Das verursacht Fehler, typischerweise um 1%%." + +#: solvespace.cpp:968 +msgid "" +"This group does not contain a correctly-formed 2d closed area. It is open, " +"not coplanar, or self-intersecting." +msgstr "" +"Diese Gruppe beinhaltet keine korrekt geschlossene 2D Fläche. Sie ist offen, " +"nicht koplanar, oder überschneidet sich selbst." + +#: solvespace.cpp:980 +#, c-format +msgid "" +"The area of the region sketched in this group is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"Die Summe der Flächen in dieser Gruppe ist:\n" +"\n" +" %s\n" +"\n" +"Kurven wurden als gerade Linienstücke angenähert.\n" +"Das verursacht Fehler, typischerweise um 1%%." + +#: solvespace.cpp:1000 +#, c-format +msgid "" +"The total length of the selected entities is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"Die Gesamtlänge der ausgwählten Elemente ist:\n" +"\n" +" %s\n" +"\n" +"Kurven wurden als gerade Linienstücke angenähert.\n" +"Das verursacht Fehler, typischerweise um 1%%." + +#: solvespace.cpp:1006 +msgid "Bad selection for perimeter; select line segments, arcs, and curves." +msgstr "Falsche Auswahl für Umfang. Wähle Liniensegmente, Bögen und Kurven." + +#: solvespace.cpp:1022 +msgid "Bad selection for trace; select a single point." +msgstr "Falsche Auswahl für Punkt nachzeichnen. Wähle einen einzelnen Punkt." + +#: solvespace.cpp:1049 +#, c-format +msgid "Couldn't write to '%s'" +msgstr "Konnte '%s' nicht schreiben" + +#: solvespace.cpp:1079 +msgid "The mesh is self-intersecting (NOT okay, invalid)." +msgstr "Das Netz schneidet sich selbst: Falsch, ungültig." + +#: solvespace.cpp:1080 +msgid "The mesh is not self-intersecting (okay, valid)." +msgstr "Das Netz schneidet sich nicht: Gut, gültig." + +#: solvespace.cpp:1082 +msgid "The mesh has naked edges (NOT okay, invalid)." +msgstr "Das Netz hat lose Kanten: Falsch, ungültig." + +#: solvespace.cpp:1083 +msgid "The mesh is watertight (okay, valid)." +msgstr "Das Netz hat keine lose Kanten: Gut, gültig." + +#: solvespace.cpp:1086 +#, c-format +msgid "" +"\n" +"\n" +"The model contains %d triangles, from %d surfaces." +msgstr "" +"\n" +"\n" +"Das Modell hat %d Dreiecke, von %d Flächen." + +#: solvespace.cpp:1090 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"Zero problematic edges, good.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"Keine problematischen Kanten, gut.%s" + +#: solvespace.cpp:1093 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d problematic edges, bad.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d problematische Kanten, schlecht.%s" + +#: solvespace.cpp:1106 +#, c-format +msgid "" +"This is SolveSpace version %s.\n" +"\n" +"For more information, see http://solvespace.com/\n" +"\n" +"SolveSpace is free software: you are free to modify\n" +"and/or redistribute it under the terms of the GNU\n" +"General Public License (GPL) version 3 or later.\n" +"\n" +"There is NO WARRANTY, to the extent permitted by\n" +"law. For details, visit http://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues and other authors.\n" +msgstr "" +"Das ist SolveSpace version %s.\n" +"\n" +"Für mehr Information siehe http://solvespace.com/\n" +"\n" +"SolveSpace ist freie Software: Sie steht Ihnen frei sie zu ändern\n" +"und/oder zu vervielfältigen unter der Auflage der GNU\n" +"General Public License (GPL) Version 3 oder späterer Versionen.\n" +"\n" +"Es besteht keinerlei Gewährleistung und Haftung für das Programm, soweit " +"dies gesetzlich zulässig ist.\n" +"Für Details siehe http://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues und andere.\n" + +#: style.cpp:185 +msgid "" +"Can't assign style to an entity that's derived from another entity; try " +"assigning a style to this entity's parent." +msgstr "" +"Ein Linientyp kann keinem Objekt zugeordnet werden, das von einem anderen " +"Objekt abgeleitet wurde. Versuchen Sie, dem übergeordneten Objekt einen Typ " +"zuzuordnen." + +#: style.cpp:735 +msgid "Style name cannot be empty" +msgstr "Name des Linientyps kann nicht leer sein" + +#: textscreens.cpp:837 +msgid "Can't repeat fewer than 1 time." +msgstr "Nicht weniger als 1 Wiederholung möglich." + +#: textscreens.cpp:841 +msgid "Can't repeat more than 999 times." +msgstr "Nicht mehr als 999 Wiederholungen möglich." + +#: textscreens.cpp:866 +msgid "Group name cannot be empty" +msgstr "Der Name der Gruppe darf nicht leer sein" + +#: textscreens.cpp:918 +msgid "Opacity must be between zero and one." +msgstr "Durchsichtigkeit muss zwischen Null und Eins sein." + +#: textscreens.cpp:953 +msgid "Radius cannot be zero or negative." +msgstr "Radius darf nicht null oder negativ sein." + +#: toolbar.cpp:18 +msgid "Sketch line segment" +msgstr "Liniensegment erstellen" + +#: toolbar.cpp:20 +msgid "Sketch rectangle" +msgstr "Rechteck erstellen" + +#: toolbar.cpp:22 +msgid "Sketch circle" +msgstr "Kreis erstellen" + +#: toolbar.cpp:24 +msgid "Sketch arc of a circle" +msgstr "Kreisbogen erstellen" + +#: toolbar.cpp:26 +msgid "Sketch curves from text in a TrueType font" +msgstr "Kurven aus Text in einem TrueType-Font erzeugen" + +#: toolbar.cpp:28 +msgid "Sketch image from a file" +msgstr "Skizze von einer Bilddatei erstellen" + +#: toolbar.cpp:30 +msgid "Create tangent arc at selected point" +msgstr "Bogentangente im ausgewählten Punkt erstellen" + +#: toolbar.cpp:32 +msgid "Sketch cubic Bezier spline" +msgstr "Kubischen Bezier-Spline erstellen" + +#: toolbar.cpp:34 +msgid "Sketch datum point" +msgstr "Bezugspunkt erstellen" + +#: toolbar.cpp:36 +msgid "Toggle construction" +msgstr "Konstruktionselement an/aus" + +#: toolbar.cpp:38 +msgid "Split lines / curves where they intersect" +msgstr "Linien / Kurven im Schnittpunkt trennen" + +#: toolbar.cpp:42 +msgid "Constrain distance / diameter / length" +msgstr "Abstand / Durchmesser / Länge einschränken" + +#: toolbar.cpp:44 +msgid "Constrain angle" +msgstr "Winkel einschränken" + +#: toolbar.cpp:46 +msgid "Constrain to be horizontal" +msgstr "Horizontale Einschränkung" + +#: toolbar.cpp:48 +msgid "Constrain to be vertical" +msgstr "Vertikale Einschränkung" + +#: toolbar.cpp:50 +msgid "Constrain to be parallel or tangent" +msgstr "Einschränkung auf Parallele oder Tangente" + +#: toolbar.cpp:52 +msgid "Constrain to be perpendicular" +msgstr "Rechtwinklige Einschränkung" + +#: toolbar.cpp:54 +msgid "Constrain point on line / curve / plane / point" +msgstr "Punkt auf Linie / Kurve / Ebene / Punkt einschränken" + +#: toolbar.cpp:56 +msgid "Constrain symmetric" +msgstr "Symmetrische Einschränkung" + +#: toolbar.cpp:58 +msgid "Constrain equal length / radius / angle" +msgstr "Gleiche Länge / Radius / Winkel einschränken" + +#: toolbar.cpp:60 +msgid "Constrain normals in same orientation" +msgstr "Normale auf gleiche Richtung einschränken" + +#: toolbar.cpp:62 +msgid "Other supplementary angle" +msgstr "Anderer Komplementärwinkel" + +#: toolbar.cpp:64 +msgid "Toggle reference dimension" +msgstr "Von/zu Referenzangabe wechseln" + +#: toolbar.cpp:68 +msgid "New group extruding active sketch" +msgstr "Neue Gruppe mit extrudierter aktiver Skizze" + +#: toolbar.cpp:70 +msgid "New group rotating active sketch" +msgstr "Neue Gruppe mit rotierter aktiver Skizze" + +#: toolbar.cpp:72 +msgid "New group helix from active sketch" +msgstr "Neue Gruppe Helix von aktiver Skizze" + +#: toolbar.cpp:74 +msgid "New group revolve active sketch" +msgstr "Neue Gruppe mit gedrehter aktiver Skizze" + +#: toolbar.cpp:76 +msgid "New group step and repeat rotating" +msgstr "Neue Gruppe mit kopierter gedrehter Skizze" + +#: toolbar.cpp:78 +msgid "New group step and repeat translating" +msgstr "Neue Gruppe mit kopierter versetzter Skizze" + +#: toolbar.cpp:80 +msgid "New group in new workplane (thru given entities)" +msgstr "" +"Neue Gruppe in neuer Arbeitsebene (definiert durch ausgewählte Objekte)" + +#: toolbar.cpp:82 +msgid "New group in 3d" +msgstr "Neue Gruppe in 3D" + +#: toolbar.cpp:84 +msgid "New group linking / assembling file" +msgstr "Neue Gruppe mit verknüpfter Datei" + +#: toolbar.cpp:88 +msgid "Nearest isometric view" +msgstr "Nächste isometrische Ansicht" + +#: toolbar.cpp:90 +msgid "Align view to active workplane" +msgstr "Ansicht auf Arbeitsebene ausrichten" + +#: util.cpp:165 +msgctxt "title" +msgid "Error" +msgstr "Fehler" + +#: util.cpp:165 +msgctxt "title" +msgid "Message" +msgstr "Mitteilung" + +#: util.cpp:170 +msgctxt "button" +msgid "&OK" +msgstr "&OK" + +#: view.cpp:127 +msgid "Scale cannot be zero or negative." +msgstr "Der Maßstab kann nicht Null oder negativ sein." + +#: view.cpp:139 view.cpp:148 +msgid "Bad format: specify x, y, z" +msgstr "Ungültiges Format: geben Sie x, y, z ein" + +#~ msgid "" +#~ "Bad selection for on point / curve / plane constraint. This constraint " +#~ "can apply to:\n" +#~ "\n" +#~ " * two points (points coincident)\n" +#~ " * a point and a workplane (point in plane)\n" +#~ " * a point and a line segment (point on line)\n" +#~ " * a point and a circle or arc (point on curve)\n" +#~ " * a point and a plane face (point on face)\n" +#~ msgstr "" +#~ "Ungültige Auswahl für Einschränkung \"Auf Punkt / Kurve / Ebene\". Diese " +#~ "Einschränkung ist anwendbar auf:\n" +#~ "\n" +#~ " * zwei Punkte [deckungsgleich]\n" +#~ " * einen Punkt und eine Arbeitsebene [Punkt auf Ebene]\n" +#~ " * einen Punkt und ein Liniensegment [Punkt auf Linie]\n" +#~ " * einen Punkt und einen Kreis oder Bogen [Punkt auf Kurve]\n" +#~ " * einen Punkt und eine Seitenfläche [Punkt auf Fläche]\n" + +#~ msgid "" +#~ "Bad selection for equal length / radius constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two line segments (equal length)\n" +#~ " * two line segments and two points (equal point-line distances)\n" +#~ " * a line segment and two points (equal point-line distances)\n" +#~ " * a line segment, and a point and line segment (point-line distance " +#~ "equals length)\n" +#~ " * four line segments or normals (equal angle between A,B and C,D)\n" +#~ " * three line segments or normals (equal angle between A,B and B,C)\n" +#~ " * two circles or arcs (equal radius)\n" +#~ " * a line segment and an arc (line segment length equals arc length)\n" +#~ msgstr "" +#~ "Ungültige Auswahl für Einschränkung \"gleicher Abstand / Radius\". Diese " +#~ "Einschränkung ist anwendbar auf:\n" +#~ "\n" +#~ " * zwei Liniensegmente [gleiche Länge]\n" +#~ " * zwei Liniensegmente und zwei Punkte [gleiche Punkt-Linien-" +#~ "Abstände]\n" +#~ " * ein Liniensegment und zwei Punkte [gleiche Punkt-Linien-Abstände]\n" +#~ " * ein Liniensegment und ein Punkt oder Liniensegment [Abstand Punkt-" +#~ "Linie gleich Länge]\n" +#~ " * vier Liniensegmente oder Normale [gleicher Winkel zwischen A,B und " +#~ "C,D]\n" +#~ " * drei Liniensegmente oder Normale [gleicher Winkel zwischen A,B und " +#~ "B,C]\n" +#~ " * zwei Kreise oder Bögen [gleicher Radius]\n" +#~ " * ein Liniensegment und ein Bogen [Länge des Liniensegments gleich " +#~ "Bogenlänge]\n" + +#~ msgid "" +#~ "Bad selection for horizontal / vertical constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two points\n" +#~ " * a line segment\n" +#~ msgstr "" +#~ "Ungültige Auswahl für Einschränkung \"horizontal / vertikal\". Diese " +#~ "Einschränkung ist anwendbar auf:\n" +#~ "\n" +#~ " * zwei Punkte\n" +#~ " * ein Liniensegment\n" + +#~ msgid "" +#~ "Bad selection for angle constraint. This constraint can apply to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ " * a line segment and a normal\n" +#~ " * two normals\n" +#~ msgstr "" +#~ "Ungültige Auswahl für Einschränkung \"Winkel\". Diese Einschränkung ist " +#~ "anwendbar auf:\n" +#~ "\n" +#~ " * zwei Liniensegmente\n" +#~ " * ein Liniensegment und eine Normale\n" +#~ " * zwei Normale\n" + +#~ msgid "" +#~ "Bad selection for parallel / tangent constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two line segments (parallel)\n" +#~ " * a line segment and a normal (parallel)\n" +#~ " * two normals (parallel)\n" +#~ " * two line segments, arcs, or beziers, that share an endpoint " +#~ "(tangent)\n" +#~ msgstr "" +#~ "Ungültige Auswahl für Einschränkung \"Parallel / Tangente\". Diese " +#~ "Einschränkung ist anwendbar auf:\n" +#~ "\n" +#~ " * zwei Liniensegmente [parallel]\n" +#~ " * ein Liniensegment und eine Normale [parallel]\n" +#~ " * zwei Normalen [parallel]\n" +#~ " * zwei Liniensegmente, Bögen oder Beziers mit gemeinsamem Endpunkt " +#~ "[Tangente]\n" + +#~ msgid "" +#~ "Bad selection for perpendicular constraint. This constraint can apply " +#~ "to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ " * a line segment and a normal\n" +#~ " * two normals\n" +#~ msgstr "" +#~ "Ungültige Auswahl für Einschränkung \"Rechtwinklig\". Diese Einschränkung " +#~ "ist anwendbar auf:\n" +#~ "\n" +#~ " * zwei Liniensegmente\n" +#~ " * ein Liniensegment und eine Normale\n" +#~ " * zwei Normale\n" + +#~ msgid "A&ngle" +#~ msgstr "Winkel" + +#~ msgid "E&qual Length / Radius / Angle" +#~ msgstr "Gleicher Abstand / Radius / Winkel" + +#~ msgid "" +#~ "Bad selection for length ratio constraint. This constraint can apply to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ msgstr "" +#~ "Ungültige Auswahl für Einschränkung \"Längenverhältnis\". Diese " +#~ "Einschränkung ist anwendbar auf:\n" +#~ "\n" +#~ " * zwei Liniensegmente\n" + +#~ msgid "" +#~ "Bad selection for length difference constraint. This constraint can apply " +#~ "to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ msgstr "" +#~ "Ungültige Auswahl für Einschränkung \"Längendifferenz\". Diese " +#~ "Einschränkung ist anwendbar auf:\n" +#~ "\n" +#~ " * zwei Liniensegmente\n" + +#~ msgid "Length Ra&tio" +#~ msgstr "Längenverhältnis" + +#~ msgid "Length Diff&erence" +#~ msgstr "Längendifferenz" + +#~ msgid "" +#~ "Bad selection for new sketch in workplane. This group can be created " +#~ "with:\n" +#~ "\n" +#~ " * a point (through the point, orthogonal to coordinate axes)\n" +#~ " * a point and two line segments (through the point, parallel to the " +#~ "lines)\n" +#~ " * a workplane (copy of the workplane)\n" +#~ msgstr "" +#~ "Ungültige Auswahl für Skizze in neuer Arbeitsebene. Diese Gruppe kann " +#~ "erstellt werden mit:\n" +#~ "\n" +#~ " * einem Punkt (durch den Punkt, orthogonal zu den Koordinatenachsen)\n" +#~ " * einem Punkt und zwei Liniensegmenten (durch den Punkt, parallel zu " +#~ "den Linien)\n" +#~ " * einer Arbeitsebene (Kopie der Arbeitsebene)\n" + +#~ msgctxt "file-type" +#~ msgid "Q3D Object file" +#~ msgstr "Q3D Objektdatei" + +#~ msgid "Specify between 0 and 8 digits after the decimal." +#~ msgstr "Geben Sie 0 bis 8 Ziffern nach dem Dezimalzeichen an." + +#~ msgid "click to place bottom left of text" +#~ msgstr "Klicken Sie auf die untere linke Ecke des Texts" + +#~ msgid "Do you want to save the changes you made to the new sketch?" +#~ msgstr "Möchten Sie die Änderungen in Ihrer Skizze speichern?" + +#~ msgid "Your changes will be lost if you don't save them." +#~ msgstr "" +#~ "Ihre Änderungen werden verworfen, wenn sie nicht abgespeichert werden." + +#~ msgctxt "button" +#~ msgid "Save" +#~ msgstr "Speichern" + +#~ msgctxt "button" +#~ msgid "Cancel" +#~ msgstr "Abbrechen" + +#~ msgctxt "button" +#~ msgid "Don't Save" +#~ msgstr "Nicht speichern" + +#~ msgid "An autosave file is available for this project." +#~ msgstr "" +#~ "Eine automatisch gespeicherte Datei ist für dieses Projekt vorhanden." + +#~ msgid "Do you want to load the autosave file instead?" +#~ msgstr "Möchten Sie die automatische Speicherdatei stattdessen öffnen?" + +#~ msgctxt "button" +#~ msgid "Load" +#~ msgstr "Öffnen" + +#~ msgctxt "button" +#~ msgid "Don't Load" +#~ msgstr "Nicht öffnen" + +#~ msgid "" +#~ "Do you want to locate it manually?\n" +#~ "If you select “No”, any geometry that depends on the missing file will be " +#~ "removed." +#~ msgstr "" +#~ "Möchten Sie sie selber auswählen?\n" +#~ "Falls Sie \"Nein\" wählen, wird jegliche mit der fehlenden Datei " +#~ "verknüpfte Geometrie verworfen." + +#~ msgctxt "button" +#~ msgid "Yes" +#~ msgstr "Ja" + +#~ msgctxt "button" +#~ msgid "No" +#~ msgstr "Nein" + +#~ msgctxt "button" +#~ msgid "OK" +#~ msgstr "OK" + +#~ msgid "_Cancel" +#~ msgstr "_Abbrechen" + +#~ msgid "_Open" +#~ msgstr "_Öffnen" + +#~ msgid "" +#~ "The file has changed since it was last saved.\n" +#~ "\n" +#~ "Do you want to save the changes?" +#~ msgstr "" +#~ "Die Datei wurde seit der letzten Speicherung geändert.\n" +#~ "\n" +#~ "Möchten Sie die Änderungen speichern?" + +#~ msgctxt "button" +#~ msgid "Do_n't Save" +#~ msgstr "Nicht speichern" + +#~ msgid "" +#~ "An autosave file is available for this project.\n" +#~ "\n" +#~ "Do you want to load the autosave file instead?" +#~ msgstr "" +#~ "Eine automatisch gespeicherte Datei ist für dieses Projekt vorhanden.\n" +#~ "\n" +#~ "Wollen Sie die automatisch gespeicherte Datei öffnen?" + +#~ msgctxt "button" +#~ msgid "_Load autosave" +#~ msgstr "AutoDatei öffnen" + +#~ msgctxt "button" +#~ msgid "Do_n't Load" +#~ msgstr "Nicht öffnen" + +#~ msgctxt "button" +#~ msgid "_Yes" +#~ msgstr "_Ja" + +#~ msgctxt "button" +#~ msgid "_No" +#~ msgstr "_Nein" diff --git a/res/locales/en_US.po b/res/locales/en_US.po new file mode 100644 index 0000000..4de392b --- /dev/null +++ b/res/locales/en_US.po @@ -0,0 +1,2518 @@ +# English translations for SolveSpace package. +# Copyright (C) 2017 the SolveSpace authors +# This file is distributed under the same license as the SolveSpace package. +# Automatically generated, 2017. +# +msgid "" +msgstr "" +"Project-Id-Version: SolveSpace 3.0\n" +"Report-Msgid-Bugs-To: phkahler@gmail.com\n" +"POT-Creation-Date: 2025-01-26 21:04+0200\n" +"PO-Revision-Date: 2017-01-05 10:30+0000\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: en_US\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: clipboard.cpp:314 +msgid "" +"Cut, paste, and copy work only in a workplane.\n" +"\n" +"Activate one with Sketch -> In Workplane." +msgstr "" +"Cut, paste, and copy work only in a workplane.\n" +"\n" +"Activate one with Sketch -> In Workplane." + +#: clipboard.cpp:331 +msgid "Clipboard is empty; nothing to paste." +msgstr "Clipboard is empty; nothing to paste." + +#: clipboard.cpp:378 +msgid "Number of copies to paste must be at least one." +msgstr "Number of copies to paste must be at least one." + +#: clipboard.cpp:394 textscreens.cpp:879 +msgid "Scale cannot be zero." +msgstr "Scale cannot be zero." + +#: clipboard.cpp:436 +msgid "Select one point to define origin of rotation." +msgstr "Select one point to define origin of rotation." + +#: clipboard.cpp:448 +msgid "Select two points to define translation vector." +msgstr "Select two points to define translation vector." + +#: clipboard.cpp:458 +msgid "" +"Transformation is identity. So all copies will be exactly on top of each " +"other." +msgstr "" +"Transformation is identity. So all copies will be exactly on top of each " +"other." + +#: clipboard.cpp:462 +msgid "Too many items to paste; split this into smaller pastes." +msgstr "Too many items to paste; split this into smaller pastes." + +#: clipboard.cpp:467 +msgid "No workplane active." +msgstr "No workplane active." + +#: confscreen.cpp:410 +msgid "Bad format: specify coordinates as x, y, z" +msgstr "Bad format: specify coordinates as x, y, z" + +#: confscreen.cpp:420 style.cpp:729 textscreens.cpp:910 +msgid "Bad format: specify color as r, g, b" +msgstr "Bad format: specify color as r, g, b" + +#: confscreen.cpp:446 +msgid "" +"The perspective factor will have no effect until you enable View -> Use " +"Perspective Projection." +msgstr "" +"The perspective factor will have no effect until you enable View -> Use " +"Perspective Projection." + +#: confscreen.cpp:464 confscreen.cpp:474 +#, c-format +msgid "Specify between 0 and %d digits after the decimal." +msgstr "Specify between 0 and %d digits after the decimal." + +#: confscreen.cpp:486 +msgid "Export scale must not be zero!" +msgstr "Export scale must not be zero!" + +#: confscreen.cpp:498 +msgid "Cutter radius offset must not be negative!" +msgstr "Cutter radius offset must not be negative!" + +#: confscreen.cpp:557 +msgid "Bad value: autosave interval should be positive" +msgstr "Bad value: autosave interval should be positive" + +#: confscreen.cpp:560 +msgid "Bad format: specify interval in integral minutes" +msgstr "Bad format: specify interval in integral minutes" + +#: constraint.cpp:12 +msgctxt "constr-name" +msgid "pts-coincident" +msgstr "pts-coincident" + +#: constraint.cpp:13 +msgctxt "constr-name" +msgid "pt-pt-distance" +msgstr "pt-pt-distance" + +#: constraint.cpp:14 +msgctxt "constr-name" +msgid "pt-line-distance" +msgstr "pt-line-distance" + +#: constraint.cpp:15 +msgctxt "constr-name" +msgid "pt-plane-distance" +msgstr "pt-plane-distance" + +#: constraint.cpp:16 +msgctxt "constr-name" +msgid "pt-face-distance" +msgstr "pt-face-distance" + +#: constraint.cpp:17 +msgctxt "constr-name" +msgid "proj-pt-pt-distance" +msgstr "proj-pt-pt-distance" + +#: constraint.cpp:18 +msgctxt "constr-name" +msgid "pt-in-plane" +msgstr "pt-in-plane" + +#: constraint.cpp:19 +msgctxt "constr-name" +msgid "pt-on-line" +msgstr "pt-on-line" + +#: constraint.cpp:20 +msgctxt "constr-name" +msgid "pt-on-face" +msgstr "pt-on-face" + +#: constraint.cpp:21 +msgctxt "constr-name" +msgid "eq-length" +msgstr "eq-length" + +#: constraint.cpp:22 +msgctxt "constr-name" +msgid "eq-length-and-pt-ln-dist" +msgstr "eq-length-and-pt-ln-dist" + +#: constraint.cpp:23 +msgctxt "constr-name" +msgid "eq-pt-line-distances" +msgstr "eq-pt-line-distances" + +#: constraint.cpp:24 +msgctxt "constr-name" +msgid "length-ratio" +msgstr "length-ratio" + +#: constraint.cpp:25 +msgctxt "constr-name" +msgid "arc-arc-length-ratio" +msgstr "arc-arc-length-ratio" + +#: constraint.cpp:26 +msgctxt "constr-name" +msgid "arc-line-length-ratio" +msgstr "arc-line-length-ratio" + +#: constraint.cpp:27 +msgctxt "constr-name" +msgid "length-difference" +msgstr "length-difference" + +#: constraint.cpp:28 +msgctxt "constr-name" +msgid "arc-arc-len-difference" +msgstr "arc-arc-len-difference" + +#: constraint.cpp:29 +msgctxt "constr-name" +msgid "arc-line-len-difference" +msgstr "arc-line-len-difference" + +#: constraint.cpp:30 +msgctxt "constr-name" +msgid "symmetric" +msgstr "symmetric" + +#: constraint.cpp:31 +msgctxt "constr-name" +msgid "symmetric-h" +msgstr "symmetric-h" + +#: constraint.cpp:32 +msgctxt "constr-name" +msgid "symmetric-v" +msgstr "symmetric-v" + +#: constraint.cpp:33 +msgctxt "constr-name" +msgid "symmetric-line" +msgstr "symmetric-line" + +#: constraint.cpp:34 +msgctxt "constr-name" +msgid "at-midpoint" +msgstr "at-midpoint" + +#: constraint.cpp:35 +msgctxt "constr-name" +msgid "horizontal" +msgstr "horizontal" + +#: constraint.cpp:36 +msgctxt "constr-name" +msgid "vertical" +msgstr "vertical" + +#: constraint.cpp:37 +msgctxt "constr-name" +msgid "diameter" +msgstr "diameter" + +#: constraint.cpp:38 +msgctxt "constr-name" +msgid "pt-on-circle" +msgstr "pt-on-circle" + +#: constraint.cpp:39 +msgctxt "constr-name" +msgid "same-orientation" +msgstr "same-orientation" + +#: constraint.cpp:40 +msgctxt "constr-name" +msgid "angle" +msgstr "angle" + +#: constraint.cpp:41 +msgctxt "constr-name" +msgid "parallel" +msgstr "parallel" + +#: constraint.cpp:42 +msgctxt "constr-name" +msgid "arc-line-tangent" +msgstr "arc-line-tangent" + +#: constraint.cpp:43 +msgctxt "constr-name" +msgid "cubic-line-tangent" +msgstr "cubic-line-tangent" + +#: constraint.cpp:44 +msgctxt "constr-name" +msgid "curve-curve-tangent" +msgstr "curve-curve-tangent" + +#: constraint.cpp:45 +msgctxt "constr-name" +msgid "perpendicular" +msgstr "perpendicular" + +#: constraint.cpp:46 +msgctxt "constr-name" +msgid "eq-radius" +msgstr "eq-radius" + +#: constraint.cpp:47 +msgctxt "constr-name" +msgid "eq-angle" +msgstr "eq-angle" + +#: constraint.cpp:48 +msgctxt "constr-name" +msgid "eq-line-len-arc-len" +msgstr "eq-line-len-arc-len" + +#: constraint.cpp:49 +msgctxt "constr-name" +msgid "lock-where-dragged" +msgstr "lock-where-dragged" + +#: constraint.cpp:50 +msgctxt "constr-name" +msgid "comment" +msgstr "comment" + +#: constraint.cpp:151 +msgid "" +"The point you selected does not belong to the arc. The arc and line segment " +"do not share an end point.\n" +"\n" +"Select the end point of the arc at which you want it to be tangent to the " +"line." +msgstr "" +"The point you selected does not belong to the arc. The arc and line segment " +"do not share an end point.\n" +"\n" +"Select the end point of the arc at which you want it to be tangent to the " +"line." + +#: constraint.cpp:158 +msgid "" +"The tangent arc and line segment must share an endpoint. Constrain them with " +"Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the arc at which you want it to be " +"tangent to the line." +msgstr "" +"The tangent arc and line segment must share an endpoint. Constrain them with " +"Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the arc at which you want it to be " +"tangent to the line." + +#: constraint.cpp:186 +msgid "" +"The point you selected is not an end point of the cubic spline. The spline " +"and line segment do not share an end point.\n" +"\n" +"Select the end point of the spline at which you want it to be tangent to the " +"line." +msgstr "" +"The point you selected is not an end point of the cubic spline. The spline " +"and line segment do not share an end point.\n" +"\n" +"Select the end point of the spline at which you want it to be tangent to the " +"line." + +#: constraint.cpp:193 +msgid "" +"The tangent cubic spline and line segment must share an endpoint. Constrain " +"them with Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the cubic spline at which you want it " +"to be tangent to the line." +msgstr "" +"The tangent cubic spline and line segment must share an endpoint. Constrain " +"them with Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the cubic spline at which you want it " +"to be tangent to the line." + +#: constraint.cpp:237 +msgid "" +"The points you selected are not end points of the two curves. The curves do " +"not share an end point.\n" +"\n" +"Select the end points of both curves at which you want them to be tangent to " +"each other." +msgstr "" +"The points you selected are not end points of the two curves. The curves do " +"not share an end point.\n" +"\n" +"Select the end points of both curves at which you want them to be tangent to " +"each other." + +#: constraint.cpp:244 +msgid "" +"The curves must share an endpoint. Constrain them with Constrain -> On Point " +"before constraining tangent.\n" +"\n" +"Alternatively select the end points of both curves at which you want the " +"curves to be tangent." +msgstr "" +"The curves must share an endpoint. Constrain them with Constrain -> On Point " +"before constraining tangent.\n" +"\n" +"Alternatively select the end points of both curves at which you want the " +"curves to be tangent." + +#: constraint.cpp:303 +msgid "" +"Bad selection for distance / diameter constraint. This constraint can apply " +"to:\n" +"\n" +" * two points (distance between points)\n" +" * a line segment (length)\n" +" * two points and a line segment or normal (projected distance)\n" +" * a workplane and a point (minimum distance)\n" +" * a line segment and a point (minimum distance)\n" +" * a plane face and a point (minimum distance)\n" +" * a circle or an arc (diameter)\n" +msgstr "" +"Bad selection for distance / diameter constraint. This constraint can apply " +"to:\n" +"\n" +" * two points (distance between points)\n" +" * a line segment (length)\n" +" * two points and a line segment or normal (projected distance)\n" +" * a workplane and a point (minimum distance)\n" +" * a line segment and a point (minimum distance)\n" +" * a plane face and a point (minimum distance)\n" +" * a circle or an arc (diameter)\n" + +#: constraint.cpp:366 +msgid "" +"Bad selection for on point / curve / plane constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points (points coincident)\n" +" * a point and a workplane (point in plane)\n" +" * a point and a line segment (point on line)\n" +" * a point and a circle or arc (point on curve)\n" +" * a point and one to three plane faces (point on face(s))\n" +msgstr "" +"Bad selection for on point / curve / plane constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points (points coincident)\n" +" * a point and a workplane (point in plane)\n" +" * a point and a line segment (point on line)\n" +" * a point and a circle or arc (point on curve)\n" +" * a point and one to three plane faces (point on face(s))\n" + +#: constraint.cpp:427 +msgid "" +"Bad selection for equal length / radius constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more line segments (equal length)\n" +" * two line segments and two points (equal point-line distances)\n" +" * a line segment and two points (equal point-line distances)\n" +" * a line segment, and a point and line segment (point-line distance " +"equals length)\n" +" * two or more circles or arcs (equal radius)\n" +" * a line segment and an arc (line segment length equals arc length)\n" +msgstr "" +"Bad selection for equal length / radius constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more line segments (equal length)\n" +" * two line segments and two points (equal point-line distances)\n" +" * a line segment and two points (equal point-line distances)\n" +" * a line segment, and a point and line segment (point-line distance " +"equals length)\n" +" * two or more circles or arcs (equal radius)\n" +" * a line segment and an arc (line segment length equals arc length)\n" + +#: constraint.cpp:480 +msgid "" +"Bad selection for length ratio constraint. This constraint can apply to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"Bad selection for length ratio constraint. This constraint can apply to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" + +#: constraint.cpp:515 +msgid "" +"Bad selection for length difference constraint. This constraint can apply " +"to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"Bad selection for length difference constraint. This constraint can apply " +"to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" + +#: constraint.cpp:550 +msgid "" +"Bad selection for at midpoint constraint. This constraint can apply to:\n" +"\n" +" * a line segment and a point (point at midpoint)\n" +" * a line segment and a workplane (line's midpoint on plane)\n" +msgstr "" +"Bad selection for at midpoint constraint. This constraint can apply to:\n" +"\n" +" * a line segment and a point (point at midpoint)\n" +" * a line segment and a workplane (line's midpoint on plane)\n" + +#: constraint.cpp:608 +msgid "" +"Bad selection for symmetric constraint. This constraint can apply to:\n" +"\n" +" * two points or a line segment (symmetric about workplane's coordinate " +"axis)\n" +" * line segment, and two points or a line segment (symmetric about line " +"segment)\n" +" * workplane, and two points or a line segment (symmetric about " +"workplane)\n" +msgstr "" +"Bad selection for symmetric constraint. This constraint can apply to:\n" +"\n" +" * two points or a line segment (symmetric about workplane's coordinate " +"axis)\n" +" * line segment, and two points or a line segment (symmetric about line " +"segment)\n" +" * workplane, and two points or a line segment (symmetric about " +"workplane)\n" + +#: constraint.cpp:623 +msgid "" +"A workplane must be active when constraining symmetric without an explicit " +"symmetry plane." +msgstr "" +"A workplane must be active when constraining symmetric without an explicit " +"symmetry plane." + +#: constraint.cpp:663 +msgid "" +"Activate a workplane (with Sketch -> In Workplane) before applying a " +"horizontal or vertical constraint." +msgstr "" +"Activate a workplane (with Sketch -> In Workplane) before applying a " +"horizontal or vertical constraint." + +#: constraint.cpp:679 +msgid "" +"Bad selection for horizontal / vertical constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points\n" +" * one or more line segments\n" +msgstr "" +"Bad selection for horizontal / vertical constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points\n" +" * one or more line segments\n" + +#: constraint.cpp:697 +msgid "" +"Bad selection for same orientation constraint. This constraint can apply " +"to:\n" +"\n" +" * two normals\n" +msgstr "" +"Bad selection for same orientation constraint. This constraint can apply " +"to:\n" +"\n" +" * two normals\n" + +#: constraint.cpp:748 +msgid "Must select an angle constraint." +msgstr "Must select an angle constraint." + +#: constraint.cpp:761 +msgid "Must select a constraint with associated label." +msgstr "Must select a constraint with associated label." + +#: constraint.cpp:784 +msgid "" +"Bad selection for angle constraint. This constraint can apply to:\n" +"\n" +"Angle between:\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +"\n" +"Equal angles:\n" +" * four line segments or normals (equal angle between A,B and C,D)\n" +" * three line segments or normals (equal angle between A,B and B,C)\n" +msgstr "" +"Bad selection for angle constraint. This constraint can apply to:\n" +"\n" +"Angle between:\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +"\n" +"Equal angles:\n" +" * four line segments or normals (equal angle between A,B and C,D)\n" +" * three line segments or normals (equal angle between A,B and B,C)\n" + +#: constraint.cpp:872 +msgid "Curve-curve tangency must apply in workplane." +msgstr "Curve-curve tangency must apply in workplane." + +#: constraint.cpp:887 +msgid "" +"Bad selection for parallel / tangent constraint. This constraint can apply " +"to:\n" +"\n" +" * two faces\n" +" * two or more line segments (parallel)\n" +" * one or more line segments and one or more normals (parallel)\n" +" * two or more normals (parallel)\n" +" * two line segments, arcs, or beziers, that share an endpoint (tangent)\n" +" * two line segments, arcs, or beziers, that do not share an endpoint and " +"the end point(s) of the curve(s) (tangent)\n" +msgstr "" +"Bad selection for parallel / tangent constraint. This constraint can apply " +"to:\n" +"\n" +" * two faces\n" +" * two or more line segments (parallel)\n" +" * one or more line segments and one or more normals (parallel)\n" +" * two or more normals (parallel)\n" +" * two line segments, arcs, or beziers, that share an endpoint (tangent)\n" +" * two line segments, arcs, or beziers, that do not share an endpoint and " +"the end point(s) of the curve(s) (tangent)\n" + +#: constraint.cpp:914 +msgid "" +"Bad selection for perpendicular constraint. This constraint can apply to:\n" +"\n" +" * two faces\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +msgstr "" +"Bad selection for perpendicular constraint. This constraint can apply to:\n" +"\n" +" * two faces\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" + +#: constraint.cpp:931 +msgid "" +"Bad selection for lock point where dragged constraint. This constraint can " +"apply to:\n" +"\n" +" * a point\n" +msgstr "" +"Bad selection for lock point where dragged constraint. This constraint can " +"apply to:\n" +"\n" +" * a point\n" + +#: constraint.cpp:946 mouse.cpp:1160 +msgid "NEW COMMENT -- DOUBLE-CLICK TO EDIT" +msgstr "NEW COMMENT -- DOUBLE-CLICK TO EDIT" + +#: constraint.cpp:952 +msgid "click center of comment text" +msgstr "click center of comment text" + +#: export.cpp:19 +msgid "" +"No solid model present; draw one with extrudes and revolves, or use Export " +"2d View to export bare lines and curves." +msgstr "" +"No solid model present; draw one with extrudes and revolves, or use Export " +"2d View to export bare lines and curves." + +#: export.cpp:61 +msgid "" +"Bad selection for export section. Please select:\n" +"\n" +" * nothing, with an active workplane (workplane is section plane)\n" +" * a face (section plane through face)\n" +" * a point and two line segments (plane through point and parallel to " +"lines)\n" +msgstr "" +"Bad selection for export section. Please select:\n" +"\n" +" * nothing, with an active workplane (workplane is section plane)\n" +" * a face (section plane through face)\n" +" * a point and two line segments (plane through point and parallel to " +"lines)\n" + +#: export.cpp:818 +msgid "Active group mesh is empty; nothing to export." +msgstr "Active group mesh is empty; nothing to export." + +#: exportvector.cpp:336 +msgid "freehand lines were replaced with continuous lines" +msgstr "freehand lines were replaced with continuous lines" + +#: exportvector.cpp:338 +msgid "zigzag lines were replaced with continuous lines" +msgstr "zigzag lines were replaced with continuous lines" + +#: exportvector.cpp:592 +msgid "" +"Some aspects of the drawing have no DXF equivalent and were not exported:\n" +msgstr "" +"Some aspects of the drawing have no DXF equivalent and were not exported:\n" + +#: exportvector.cpp:838 +msgid "" +"PDF page size exceeds 200 by 200 inches; many viewers may reject this file." +msgstr "" +"PDF page size exceeds 200 by 200 inches; many viewers may reject this file." + +#: file.cpp:44 group.cpp:91 +msgctxt "group-name" +msgid "sketch-in-plane" +msgstr "sketch-in-plane" + +#: file.cpp:62 +msgctxt "group-name" +msgid "#references" +msgstr "#references" + +#: file.cpp:555 +msgid "The file is empty. It may be corrupt." +msgstr "The file is empty. It may be corrupt." + +#: file.cpp:560 +msgid "" +"Unrecognized data in file. This file may be corrupt, or from a newer version " +"of the program." +msgstr "" +"Unrecognized data in file. This file may be corrupt, or from a newer version " +"of the program." + +#: file.cpp:876 +msgctxt "title" +msgid "Missing File" +msgstr "Missing File" + +#: file.cpp:877 +#, c-format +msgctxt "dialog" +msgid "The linked file “%s” is not present." +msgstr "The linked file “%s” is not present." + +#: file.cpp:879 +msgctxt "dialog" +msgid "" +"Do you want to locate it manually?\n" +"\n" +"If you decline, any geometry that depends on the missing file will be " +"permanently removed." +msgstr "" +"Do you want to locate it manually?\n" +"\n" +"If you decline, any geometry that depends on the missing file will be " +"permanently removed." + +#: file.cpp:882 +msgctxt "button" +msgid "&Yes" +msgstr "&Yes" + +#: file.cpp:884 +msgctxt "button" +msgid "&No" +msgstr "&No" + +#: file.cpp:886 solvespace.cpp:652 +msgctxt "button" +msgid "&Cancel" +msgstr "&Cancel" + +#: graphicswin.cpp:41 +msgid "&File" +msgstr "&File" + +#: graphicswin.cpp:42 +msgid "&New" +msgstr "&New" + +#: graphicswin.cpp:43 +msgid "&Open..." +msgstr "&Open..." + +#: graphicswin.cpp:44 +msgid "Open &Recent" +msgstr "Open &Recent" + +#: graphicswin.cpp:45 +msgid "&Save" +msgstr "&Save" + +#: graphicswin.cpp:46 +msgid "Save &As..." +msgstr "Save &As..." + +#: graphicswin.cpp:48 +msgid "Export &Image..." +msgstr "Export &Image..." + +#: graphicswin.cpp:49 +msgid "Export 2d &View..." +msgstr "Export 2d &View..." + +#: graphicswin.cpp:50 +msgid "Export 2d &Section..." +msgstr "Export 2d &Section..." + +#: graphicswin.cpp:51 +msgid "Export 3d &Wireframe..." +msgstr "Export 3d &Wireframe..." + +#: graphicswin.cpp:52 +msgid "Export Triangle &Mesh..." +msgstr "Export Triangle &Mesh..." + +#: graphicswin.cpp:53 +msgid "Export &Surfaces..." +msgstr "Export &Surfaces..." + +#: graphicswin.cpp:54 +msgid "Im&port..." +msgstr "Im&port..." + +#: graphicswin.cpp:57 +msgid "E&xit" +msgstr "E&xit" + +#: graphicswin.cpp:60 +msgid "&Edit" +msgstr "&Edit" + +#: graphicswin.cpp:61 +msgid "&Undo" +msgstr "&Undo" + +#: graphicswin.cpp:62 +msgid "&Redo" +msgstr "&Redo" + +#: graphicswin.cpp:63 +msgid "Re&generate All" +msgstr "Re&generate All" + +#: graphicswin.cpp:65 +msgid "Snap Selection to &Grid" +msgstr "Snap Selection to &Grid" + +#: graphicswin.cpp:66 +msgid "Rotate Imported &90°" +msgstr "Rotate Imported &90°" + +#: graphicswin.cpp:68 +msgid "Cu&t" +msgstr "Cu&t" + +#: graphicswin.cpp:69 +msgid "&Copy" +msgstr "&Copy" + +#: graphicswin.cpp:70 +msgid "&Paste" +msgstr "&Paste" + +#: graphicswin.cpp:71 +msgid "Paste &Transformed..." +msgstr "Paste &Transformed..." + +#: graphicswin.cpp:72 +msgid "&Delete" +msgstr "&Delete" + +#: graphicswin.cpp:74 +msgid "Select &Edge Chain" +msgstr "Select &Edge Chain" + +#: graphicswin.cpp:75 +msgid "Select &All" +msgstr "Select &All" + +#: graphicswin.cpp:76 +msgid "&Unselect All" +msgstr "&Unselect All" + +#: graphicswin.cpp:78 +msgid "&Line Styles..." +msgstr "&Line Styles..." + +#: graphicswin.cpp:79 +msgid "&View Projection..." +msgstr "&View Projection..." + +#: graphicswin.cpp:81 +msgid "Con&figuration..." +msgstr "Con&figuration..." + +#: graphicswin.cpp:84 +msgid "&View" +msgstr "&View" + +#: graphicswin.cpp:85 +msgid "Zoom &In" +msgstr "Zoom &In" + +#: graphicswin.cpp:86 +msgid "Zoom &Out" +msgstr "Zoom &Out" + +#: graphicswin.cpp:87 +msgid "Zoom To &Fit" +msgstr "Zoom To &Fit" + +#: graphicswin.cpp:89 +msgid "Align View to &Workplane" +msgstr "Align View to &Workplane" + +#: graphicswin.cpp:90 +msgid "Nearest &Ortho View" +msgstr "Nearest &Ortho View" + +#: graphicswin.cpp:91 +msgid "Nearest &Isometric View" +msgstr "Nearest &Isometric View" + +#: graphicswin.cpp:92 +msgid "&Center View At Point" +msgstr "&Center View At Point" + +#: graphicswin.cpp:94 +msgid "Show Snap &Grid" +msgstr "Show Snap &Grid" + +#: graphicswin.cpp:95 +msgid "Darken Inactive Solids" +msgstr "Darken Inactive Solids" + +#: graphicswin.cpp:96 +msgid "Use &Perspective Projection" +msgstr "Use &Perspective Projection" + +#: graphicswin.cpp:97 +msgid "Show E&xploded View" +msgstr "Show E&xploded View" + +#: graphicswin.cpp:98 +msgid "Dimension &Units" +msgstr "Dimension &Units" + +#: graphicswin.cpp:99 +msgid "Dimensions in &Millimeters" +msgstr "Dimensions in &Millimeters" + +#: graphicswin.cpp:100 +msgid "Dimensions in M&eters" +msgstr "Dimensions in M&eters" + +#: graphicswin.cpp:101 +msgid "Dimensions in &Inches" +msgstr "Dimensions in &Inches" + +#: graphicswin.cpp:102 +msgid "Dimensions in &Feet and Inches" +msgstr "Dimensions in &Feet and Inches" + +#: graphicswin.cpp:104 +msgid "Show &Toolbar" +msgstr "Show &Toolbar" + +#: graphicswin.cpp:105 +msgid "Show Property Bro&wser" +msgstr "Show Property Bro&wser" + +#: graphicswin.cpp:107 +msgid "&Full Screen" +msgstr "&Full Screen" + +#: graphicswin.cpp:109 +msgid "&New Group" +msgstr "&New Group" + +#: graphicswin.cpp:110 +msgid "Sketch In &3d" +msgstr "Sketch In &3d" + +#: graphicswin.cpp:111 +msgid "Sketch In New &Workplane" +msgstr "Sketch In New &Workplane" + +#: graphicswin.cpp:113 +msgid "Step &Translating" +msgstr "Step &Translating" + +#: graphicswin.cpp:114 +msgid "Step &Rotating" +msgstr "Step &Rotating" + +#: graphicswin.cpp:116 +msgid "E&xtrude" +msgstr "E&xtrude" + +#: graphicswin.cpp:117 +msgid "&Helix" +msgstr "&Helix" + +#: graphicswin.cpp:118 +msgid "&Lathe" +msgstr "&Lathe" + +#: graphicswin.cpp:119 +msgid "Re&volve" +msgstr "Re&volve" + +#: graphicswin.cpp:121 +msgid "Link / Assemble..." +msgstr "Link / Assemble..." + +#: graphicswin.cpp:122 +msgid "Link Recent" +msgstr "Link Recent" + +#: graphicswin.cpp:124 +msgid "&Sketch" +msgstr "&Sketch" + +#: graphicswin.cpp:125 +msgid "In &Workplane" +msgstr "In &Workplane" + +#: graphicswin.cpp:126 +msgid "Anywhere In &3d" +msgstr "Anywhere In &3d" + +#: graphicswin.cpp:128 +msgid "Datum &Point" +msgstr "Datum &Point" + +#: graphicswin.cpp:129 +msgid "Wor&kplane" +msgstr "Wor&kplane" + +#: graphicswin.cpp:131 +msgid "Line &Segment" +msgstr "Line &Segment" + +#: graphicswin.cpp:132 +msgid "C&onstruction Line Segment" +msgstr "C&onstruction Line Segment" + +#: graphicswin.cpp:133 +msgid "&Rectangle" +msgstr "&Rectangle" + +#: graphicswin.cpp:134 +msgid "&Circle" +msgstr "&Circle" + +#: graphicswin.cpp:135 +msgid "&Arc of a Circle" +msgstr "&Arc of a Circle" + +#: graphicswin.cpp:136 +msgid "&Bezier Cubic Spline" +msgstr "&Bezier Cubic Spline" + +#: graphicswin.cpp:138 +msgid "&Text in TrueType Font" +msgstr "&Text in TrueType Font" + +#: graphicswin.cpp:139 +msgid "I&mage" +msgstr "I&mage" + +#: graphicswin.cpp:141 +msgid "To&ggle Construction" +msgstr "To&ggle Construction" + +#: graphicswin.cpp:142 +msgid "Ta&ngent Arc at Point" +msgstr "Ta&ngent Arc at Point" + +#: graphicswin.cpp:143 +msgid "Split Curves at &Intersection" +msgstr "Split Curves at &Intersection" + +#: graphicswin.cpp:145 +msgid "&Constrain" +msgstr "&Constrain" + +#: graphicswin.cpp:146 +msgid "&Distance / Diameter" +msgstr "&Distance / Diameter" + +#: graphicswin.cpp:147 +msgid "Re&ference Dimension" +msgstr "Re&ference Dimension" + +#: graphicswin.cpp:148 +msgid "A&ngle / Equal Angle" +msgstr "A&ngle / Equal Angle" + +#: graphicswin.cpp:149 +msgid "Reference An&gle" +msgstr "Reference An&gle" + +#: graphicswin.cpp:150 +msgid "Other S&upplementary Angle" +msgstr "Other S&upplementary Angle" + +#: graphicswin.cpp:151 +msgid "Toggle R&eference Dim" +msgstr "Toggle R&eference Dim" + +#: graphicswin.cpp:153 +msgid "&Horizontal" +msgstr "&Horizontal" + +#: graphicswin.cpp:154 +msgid "&Vertical" +msgstr "&Vertical" + +#: graphicswin.cpp:156 +msgid "&On Point / Curve / Plane" +msgstr "&On Point / Curve / Plane" + +#: graphicswin.cpp:157 +msgid "E&qual Length / Radius" +msgstr "E&qual Length / Radius" + +#: graphicswin.cpp:158 +msgid "Length / Arc Ra&tio" +msgstr "Length / Arc Ra&tio" + +#: graphicswin.cpp:159 +msgid "Length / Arc Diff&erence" +msgstr "Length / Arc Diff&erence" + +#: graphicswin.cpp:160 +msgid "At &Midpoint" +msgstr "At &Midpoint" + +#: graphicswin.cpp:161 +msgid "S&ymmetric" +msgstr "S&ymmetric" + +#: graphicswin.cpp:162 +msgid "Para&llel / Tangent" +msgstr "Para&llel / Tangent" + +#: graphicswin.cpp:163 +msgid "&Perpendicular" +msgstr "&Perpendicular" + +#: graphicswin.cpp:164 +msgid "Same Orient&ation" +msgstr "Same Orient&ation" + +#: graphicswin.cpp:165 +msgid "Lock Point Where &Dragged" +msgstr "Lock Point Where &Dragged" + +#: graphicswin.cpp:167 +msgid "Comment" +msgstr "Comment" + +#: graphicswin.cpp:169 +msgid "&Analyze" +msgstr "&Analyze" + +#: graphicswin.cpp:170 +msgid "Measure &Volume" +msgstr "Measure &Volume" + +#: graphicswin.cpp:171 +msgid "Measure A&rea" +msgstr "Measure A&rea" + +#: graphicswin.cpp:172 +msgid "Measure &Perimeter" +msgstr "Measure &Perimeter" + +#: graphicswin.cpp:173 +msgid "Show &Interfering Parts" +msgstr "Show &Interfering Parts" + +#: graphicswin.cpp:174 +msgid "Show &Naked Edges" +msgstr "Show &Naked Edges" + +#: graphicswin.cpp:175 +msgid "Show &Center of Mass" +msgstr "Show &Center of Mass" + +#: graphicswin.cpp:177 +msgid "Show &Underconstrained Points" +msgstr "Show &Underconstrained Points" + +#: graphicswin.cpp:179 +msgid "&Trace Point" +msgstr "&Trace Point" + +#: graphicswin.cpp:180 +msgid "&Stop Tracing..." +msgstr "&Stop Tracing..." + +#: graphicswin.cpp:181 +msgid "Step &Dimension..." +msgstr "Step &Dimension..." + +#: graphicswin.cpp:183 +msgid "&Help" +msgstr "&Help" + +#: graphicswin.cpp:184 +msgid "&Language" +msgstr "&Language" + +#: graphicswin.cpp:185 +msgid "&Website / Manual" +msgstr "&Website / Manual" + +#: graphicswin.cpp:186 +msgid "&Go to GitHub commit" +msgstr "&Go to GitHub commit" + +#: graphicswin.cpp:188 +msgid "&About" +msgstr "&About" + +#: graphicswin.cpp:362 +msgid "(no recent files)" +msgstr "(no recent files)" + +#: graphicswin.cpp:370 +#, c-format +msgid "File '%s' does not exist." +msgstr "File '%s' does not exist." + +#: graphicswin.cpp:779 +msgid "No workplane is active, so the grid will not appear." +msgstr "No workplane is active, so the grid will not appear." + +#: graphicswin.cpp:794 +msgid "" +"The perspective factor is set to zero, so the view will always be a parallel " +"projection.\n" +"\n" +"For a perspective projection, modify the perspective factor in the " +"configuration screen. A value around 0.3 is typical." +msgstr "" +"The perspective factor is set to zero, so the view will always be a parallel " +"projection.\n" +"\n" +"For a perspective projection, modify the perspective factor in the " +"configuration screen. A value around 0.3 is typical." + +#: graphicswin.cpp:884 +msgid "" +"Select a point; this point will become the center of the view on screen." +msgstr "" +"Select a point; this point will become the center of the view on screen." + +#: graphicswin.cpp:1193 +msgid "No additional entities share endpoints with the selected entities." +msgstr "No additional entities share endpoints with the selected entities." + +#: graphicswin.cpp:1211 +msgid "" +"To use this command, select a point or other entity from an linked part, or " +"make a link group the active group." +msgstr "" +"To use this command, select a point or other entity from an linked part, or " +"make a link group the active group." + +#: graphicswin.cpp:1234 +msgid "" +"No workplane is active. Activate a workplane (with Sketch -> In Workplane) " +"to define the plane for the snap grid." +msgstr "" +"No workplane is active. Activate a workplane (with Sketch -> In Workplane) " +"to define the plane for the snap grid." + +#: graphicswin.cpp:1241 +msgid "" +"Can't snap these items to grid; select points, text comments, or constraints " +"with a label. To snap a line, select its endpoints." +msgstr "" +"Can't snap these items to grid; select points, text comments, or constraints " +"with a label. To snap a line, select its endpoints." + +#: graphicswin.cpp:1326 +msgid "No workplane selected. Activating default workplane for this group." +msgstr "No workplane selected. Activating default workplane for this group." + +#: graphicswin.cpp:1329 +msgid "" +"No workplane is selected, and the active group does not have a default " +"workplane. Try selecting a workplane, or activating a sketch-in-new-" +"workplane group." +msgstr "" +"No workplane is selected, and the active group does not have a default " +"workplane. Try selecting a workplane, or activating a sketch-in-new-" +"workplane group." + +#: graphicswin.cpp:1350 +msgid "" +"Bad selection for tangent arc at point. Select a single point, or select " +"nothing to set up arc parameters." +msgstr "" +"Bad selection for tangent arc at point. Select a single point, or select " +"nothing to set up arc parameters." + +#: graphicswin.cpp:1361 +msgid "click point on arc (draws anti-clockwise)" +msgstr "click point on arc (draws anti-clockwise)" + +#: graphicswin.cpp:1362 +msgid "click to place datum point" +msgstr "click to place datum point" + +#: graphicswin.cpp:1363 +msgid "click first point of line segment" +msgstr "click first point of line segment" + +#: graphicswin.cpp:1365 +msgid "click first point of construction line segment" +msgstr "click first point of construction line segment" + +#: graphicswin.cpp:1366 +msgid "click first point of cubic segment" +msgstr "click first point of cubic segment" + +#: graphicswin.cpp:1367 +msgid "click center of circle" +msgstr "click center of circle" + +#: graphicswin.cpp:1368 +msgid "click origin of workplane" +msgstr "click origin of workplane" + +#: graphicswin.cpp:1369 +msgid "click one corner of rectangle" +msgstr "click one corner of rectangle" + +#: graphicswin.cpp:1370 +msgid "click top left of text" +msgstr "click top left of text" + +#: graphicswin.cpp:1376 +msgid "click top left of image" +msgstr "click top left of image" + +#: graphicswin.cpp:1402 +msgid "" +"No entities are selected. Select entities before trying to toggle their " +"construction state." +msgstr "" +"No entities are selected. Select entities before trying to toggle their " +"construction state." + +#: group.cpp:86 +msgctxt "group-name" +msgid "sketch-in-3d" +msgstr "sketch-in-3d" + +#: group.cpp:154 +msgid "" +"Bad selection for new sketch in workplane. This group can be created with:\n" +"\n" +" * a point (through the point, orthogonal to coordinate axes)\n" +" * a point and two line segments (through the point, parallel to the " +"lines)\n" +" * a point and a normal (through the point, orthogonal to the normal)\n" +" * a workplane (copy of the workplane)\n" +msgstr "" +"Bad selection for new sketch in workplane. This group can be created with:\n" +"\n" +" * a point (through the point, orthogonal to coordinate axes)\n" +" * a point and two line segments (through the point, parallel to the " +"lines)\n" +" * a point and a normal (through the point, orthogonal to the normal)\n" +" * a workplane (copy of the workplane)\n" + +#: group.cpp:170 +msgid "" +"Activate a workplane (Sketch -> In Workplane) before extruding. The sketch " +"will be extruded normal to the workplane." +msgstr "" +"Activate a workplane (Sketch -> In Workplane) before extruding. The sketch " +"will be extruded normal to the workplane." + +#: group.cpp:179 +msgctxt "group-name" +msgid "extrude" +msgstr "extrude" + +#: group.cpp:184 +msgid "Lathe operation can only be applied to planar sketches." +msgstr "Lathe operation can only be applied to planar sketches." + +#: group.cpp:195 +msgid "" +"Bad selection for new lathe group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Bad selection for new lathe group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" + +#: group.cpp:205 +msgctxt "group-name" +msgid "lathe" +msgstr "lathe" + +#: group.cpp:210 +msgid "Revolve operation can only be applied to planar sketches." +msgstr "Revolve operation can only be applied to planar sketches." + +#: group.cpp:221 +msgid "" +"Bad selection for new revolve group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Bad selection for new revolve group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" + +#: group.cpp:233 +msgctxt "group-name" +msgid "revolve" +msgstr "revolve" + +#: group.cpp:238 +msgid "Helix operation can only be applied to planar sketches." +msgstr "Helix operation can only be applied to planar sketches." + +#: group.cpp:249 +msgid "" +"Bad selection for new helix group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Bad selection for new helix group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" + +#: group.cpp:261 +msgctxt "group-name" +msgid "helix" +msgstr "helix" + +#: group.cpp:274 +msgid "" +"Bad selection for new rotation. This group can be created with:\n" +"\n" +" * a point, while locked in workplane (rotate in plane, about that " +"point)\n" +" * a point and a line or a normal (rotate about an axis through the " +"point, and parallel to line / normal)\n" +msgstr "" +"Bad selection for new rotation. This group can be created with:\n" +"\n" +" * a point, while locked in workplane (rotate in plane, about that " +"point)\n" +" * a point and a line or a normal (rotate about an axis through the " +"point, and parallel to line / normal)\n" + +#: group.cpp:287 +msgctxt "group-name" +msgid "rotate" +msgstr "rotate" + +#: group.cpp:298 +msgctxt "group-name" +msgid "translate" +msgstr "translate" + +#: group.cpp:422 +msgid "(unnamed)" +msgstr "(unnamed)" + +#: groupmesh.cpp:710 +msgid "not closed contour, or not all same style!" +msgstr "not closed contour, or not all same style!" + +#: groupmesh.cpp:723 +msgid "points not all coplanar!" +msgstr "points not all coplanar!" + +#: groupmesh.cpp:725 +msgid "contour is self-intersecting!" +msgstr "contour is self-intersecting!" + +#: groupmesh.cpp:727 +msgid "zero-length edge!" +msgstr "zero-length edge!" + +#: importmesh.cpp:136 +msgid "Text-formated STL files are not currently supported" +msgstr "Text-formated STL files are not currently supported" + +#: modify.cpp:252 +msgid "Must be sketching in workplane to create tangent arc." +msgstr "Must be sketching in workplane to create tangent arc." + +#: modify.cpp:299 +msgid "" +"To create a tangent arc, select a point where two non-construction lines or " +"circles in this group and workplane join." +msgstr "" +"To create a tangent arc, select a point where two non-construction lines or " +"circles in this group and workplane join." + +#: modify.cpp:386 +msgid "" +"Couldn't round this corner. Try a smaller radius, or try creating the " +"desired geometry by hand with tangency constraints." +msgstr "" +"Couldn't round this corner. Try a smaller radius, or try creating the " +"desired geometry by hand with tangency constraints." + +#: modify.cpp:595 +msgid "Couldn't split this entity; lines, circles, or cubics only." +msgstr "Couldn't split this entity; lines, circles, or cubics only." + +#: modify.cpp:622 +msgid "Must be sketching in workplane to split." +msgstr "Must be sketching in workplane to split." + +#: modify.cpp:629 +msgid "" +"Select two entities that intersect each other (e.g. two lines/circles/arcs " +"or a line/circle/arc and a point)." +msgstr "" +"Select two entities that intersect each other (e.g. two lines/circles/arcs " +"or a line/circle/arc and a point)." + +#: modify.cpp:734 +msgid "Can't split; no intersection found." +msgstr "Can't split; no intersection found." + +#: mouse.cpp:558 +msgid "Assign to Style" +msgstr "Assign to Style" + +#: mouse.cpp:574 +msgid "No Style" +msgstr "No Style" + +#: mouse.cpp:577 +msgid "Newly Created Custom Style..." +msgstr "Newly Created Custom Style..." + +#: mouse.cpp:584 +msgid "Group Info" +msgstr "Group Info" + +#: mouse.cpp:604 +msgid "Style Info" +msgstr "Style Info" + +#: mouse.cpp:624 +msgid "Select Edge Chain" +msgstr "Select Edge Chain" + +#: mouse.cpp:630 +msgid "Toggle Reference Dimension" +msgstr "Toggle Reference Dimension" + +#: mouse.cpp:636 +msgid "Other Supplementary Angle" +msgstr "Other Supplementary Angle" + +#: mouse.cpp:641 +msgid "Snap to Grid" +msgstr "Snap to Grid" + +#: mouse.cpp:650 +msgid "Remove Spline Point" +msgstr "Remove Spline Point" + +#: mouse.cpp:685 +msgid "Add Spline Point" +msgstr "Add Spline Point" + +#: mouse.cpp:689 +msgid "Cannot add spline point: maximum number of points reached." +msgstr "Cannot add spline point: maximum number of points reached." + +#: mouse.cpp:714 +msgid "Toggle Construction" +msgstr "Toggle Construction" + +#: mouse.cpp:730 +msgid "Delete Point-Coincident Constraint" +msgstr "Delete Point-Coincident Constraint" + +#: mouse.cpp:748 +msgid "Cut" +msgstr "Cut" + +#: mouse.cpp:750 +msgid "Copy" +msgstr "Copy" + +#: mouse.cpp:754 +msgid "Select All" +msgstr "Select All" + +#: mouse.cpp:759 +msgid "Paste" +msgstr "Paste" + +#: mouse.cpp:761 +msgid "Paste Transformed..." +msgstr "Paste Transformed..." + +#: mouse.cpp:766 +msgid "Delete" +msgstr "Delete" + +#: mouse.cpp:769 +msgid "Unselect All" +msgstr "Unselect All" + +#: mouse.cpp:776 +msgid "Unselect Hovered" +msgstr "Unselect Hovered" + +#: mouse.cpp:785 +msgid "Zoom to Fit" +msgstr "Zoom to Fit" + +#: mouse.cpp:987 mouse.cpp:1276 +msgid "click next point of line, or press Esc" +msgstr "click next point of line, or press Esc" + +#: mouse.cpp:993 +msgid "" +"Can't draw rectangle in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Can't draw rectangle in 3d; first, activate a workplane with Sketch -> In " +"Workplane." + +#: mouse.cpp:1027 +msgid "click to place other corner of rectangle" +msgstr "click to place other corner of rectangle" + +#: mouse.cpp:1048 +msgid "click to set radius" +msgstr "click to set radius" + +#: mouse.cpp:1053 +msgid "" +"Can't draw arc in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Can't draw arc in 3d; first, activate a workplane with Sketch -> In " +"Workplane." + +#: mouse.cpp:1072 +msgid "click to place point" +msgstr "click to place point" + +#: mouse.cpp:1088 +msgid "click next point of cubic, or press Esc" +msgstr "click next point of cubic, or press Esc" + +#: mouse.cpp:1093 +msgid "" +"Sketching in a workplane already; sketch in 3d before creating new workplane." +msgstr "" +"Sketching in a workplane already; sketch in 3d before creating new workplane." + +#: mouse.cpp:1109 +msgid "" +"Can't draw text in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Can't draw text in 3d; first, activate a workplane with Sketch -> In " +"Workplane." + +#: mouse.cpp:1126 +msgid "click to place bottom right of text" +msgstr "click to place bottom right of text" + +#: mouse.cpp:1132 +msgid "" +"Can't draw image in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Can't draw image in 3d; first, activate a workplane with Sketch -> In " +"Workplane." + +#: platform/gui.cpp:85 platform/gui.cpp:90 solvespace.cpp:583 +msgctxt "file-type" +msgid "SolveSpace models" +msgstr "SolveSpace models" + +#: platform/gui.cpp:89 +msgctxt "file-type" +msgid "ALL" +msgstr "ALL" + +#: platform/gui.cpp:91 +msgctxt "file-type" +msgid "IDF circuit board" +msgstr "IDF circuit board" + +#: platform/gui.cpp:92 +msgctxt "file-type" +msgid "STL triangle mesh" +msgstr "STL triangle mesh" + +#: platform/gui.cpp:96 +msgctxt "file-type" +msgid "PNG image" +msgstr "PNG image" + +#: platform/gui.cpp:100 +msgctxt "file-type" +msgid "STL mesh" +msgstr "STL mesh" + +#: platform/gui.cpp:101 +msgctxt "file-type" +msgid "Wavefront OBJ mesh" +msgstr "Wavefront OBJ mesh" + +#: platform/gui.cpp:102 +msgctxt "file-type" +msgid "Three.js-compatible mesh, with viewer" +msgstr "Three.js-compatible mesh, with viewer" + +#: platform/gui.cpp:103 +msgctxt "file-type" +msgid "Three.js-compatible mesh, mesh only" +msgstr "Three.js-compatible mesh, mesh only" + +#: platform/gui.cpp:104 +msgctxt "file-type" +msgid "VRML text file" +msgstr "VRML text file" + +#: platform/gui.cpp:108 platform/gui.cpp:115 platform/gui.cpp:122 +msgctxt "file-type" +msgid "STEP file" +msgstr "STEP file" + +#: platform/gui.cpp:112 +msgctxt "file-type" +msgid "PDF file" +msgstr "PDF file" + +#: platform/gui.cpp:113 +msgctxt "file-type" +msgid "Encapsulated PostScript" +msgstr "Encapsulated PostScript" + +#: platform/gui.cpp:114 +msgctxt "file-type" +msgid "Scalable Vector Graphics" +msgstr "Scalable Vector Graphics" + +#: platform/gui.cpp:116 platform/gui.cpp:123 +msgctxt "file-type" +msgid "DXF file (AutoCAD 2007)" +msgstr "DXF file (AutoCAD 2007)" + +#: platform/gui.cpp:117 +msgctxt "file-type" +msgid "HPGL file" +msgstr "HPGL file" + +#: platform/gui.cpp:118 +msgctxt "file-type" +msgid "G Code" +msgstr "G Code" + +#: platform/gui.cpp:127 +msgctxt "file-type" +msgid "AutoCAD DXF and DWG files" +msgstr "AutoCAD DXF and DWG files" + +#: platform/gui.cpp:131 +msgctxt "file-type" +msgid "Comma-separated values" +msgstr "Comma-separated values" + +#: platform/guigtk.cpp:1434 platform/guimac.mm:1513 platform/guiwin.cpp:1641 +msgid "untitled" +msgstr "untitled" + +#: platform/guigtk.cpp:1445 platform/guigtk.cpp:1481 platform/guimac.mm:1471 +#: platform/guiwin.cpp:1639 +msgctxt "title" +msgid "Save File" +msgstr "Save File" + +#: platform/guigtk.cpp:1446 platform/guigtk.cpp:1482 platform/guimac.mm:1454 +#: platform/guiwin.cpp:1645 +msgctxt "title" +msgid "Open File" +msgstr "Open File" + +#: platform/guigtk.cpp:1449 platform/guigtk.cpp:1488 +msgctxt "button" +msgid "_Cancel" +msgstr "_Cancel" + +#: platform/guigtk.cpp:1450 platform/guigtk.cpp:1486 +msgctxt "button" +msgid "_Save" +msgstr "_Save" + +#: platform/guigtk.cpp:1451 platform/guigtk.cpp:1487 +msgctxt "button" +msgid "_Open" +msgstr "_Open" + +#: solvespace.cpp:175 +msgctxt "title" +msgid "Autosave Available" +msgstr "Autosave Available" + +#: solvespace.cpp:176 +msgctxt "dialog" +msgid "An autosave file is available for this sketch." +msgstr "An autosave file is available for this sketch." + +#: solvespace.cpp:177 +msgctxt "dialog" +msgid "Do you want to load the autosave file instead?" +msgstr "Do you want to load the autosave file instead?" + +#: solvespace.cpp:178 +msgctxt "button" +msgid "&Load autosave" +msgstr "&Load autosave" + +#: solvespace.cpp:180 +msgctxt "button" +msgid "Do&n't Load" +msgstr "Do&n't Load" + +#: solvespace.cpp:640 +msgctxt "title" +msgid "Modified File" +msgstr "Modified File" + +#: solvespace.cpp:642 +#, c-format +msgctxt "dialog" +msgid "Do you want to save the changes you made to the sketch “%s”?" +msgstr "Do you want to save the changes you made to the sketch “%s”?" + +#: solvespace.cpp:645 +msgctxt "dialog" +msgid "Do you want to save the changes you made to the new sketch?" +msgstr "Do you want to save the changes you made to the new sketch?" + +#: solvespace.cpp:648 +msgctxt "dialog" +msgid "Your changes will be lost if you don't save them." +msgstr "Your changes will be lost if you don't save them." + +#: solvespace.cpp:649 +msgctxt "button" +msgid "&Save" +msgstr "&Save" + +#: solvespace.cpp:651 +msgctxt "button" +msgid "Do&n't Save" +msgstr "Do&n't Save" + +#: solvespace.cpp:672 +msgctxt "title" +msgid "(new sketch)" +msgstr "(new sketch)" + +#: solvespace.cpp:683 +msgctxt "title" +msgid "Property Browser" +msgstr "Property Browser" + +#: solvespace.cpp:746 +msgid "" +"Constraints are currently shown, and will be exported in the toolpath. This " +"is probably not what you want; hide them by clicking the link at the top of " +"the text window." +msgstr "" +"Constraints are currently shown, and will be exported in the toolpath. This " +"is probably not what you want; hide them by clicking the link at the top of " +"the text window." + +#: solvespace.cpp:834 +#, c-format +msgid "" +"Can't identify file type from file extension of filename '%s'; try .dxf or ." +"dwg." +msgstr "" +"Can't identify file type from file extension of filename '%s'; try .dxf or ." +"dwg." + +#: solvespace.cpp:886 +msgid "Constraint must have a label, and must not be a reference dimension." +msgstr "Constraint must have a label, and must not be a reference dimension." + +#: solvespace.cpp:890 +msgid "Bad selection for step dimension; select a constraint." +msgstr "Bad selection for step dimension; select a constraint." + +#: solvespace.cpp:914 +msgid "The assembly does not interfere, good." +msgstr "The assembly does not interfere, good." + +#: solvespace.cpp:930 +#, c-format +msgid "" +"The volume of the solid model is:\n" +"\n" +" %s" +msgstr "" +"The volume of the solid model is:\n" +"\n" +" %s" + +#: solvespace.cpp:939 +#, c-format +msgid "" +"\n" +"The volume of current group mesh is:\n" +"\n" +" %s" +msgstr "" +"\n" +"The volume of current group mesh is:\n" +"\n" +" %s" + +#: solvespace.cpp:944 +msgid "" +"\n" +"\n" +"Curved surfaces have been approximated as triangles.\n" +"This introduces error, typically of around 1%." +msgstr "" +"\n" +"\n" +"Curved surfaces have been approximated as triangles.\n" +"This introduces error, typically of around 1%." + +#: solvespace.cpp:959 +#, c-format +msgid "" +"The surface area of the selected faces is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"The surface area of the selected faces is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." + +#: solvespace.cpp:968 +msgid "" +"This group does not contain a correctly-formed 2d closed area. It is open, " +"not coplanar, or self-intersecting." +msgstr "" +"This group does not contain a correctly-formed 2d closed area. It is open, " +"not coplanar, or self-intersecting." + +#: solvespace.cpp:980 +#, c-format +msgid "" +"The area of the region sketched in this group is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"The area of the region sketched in this group is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." + +#: solvespace.cpp:1000 +#, c-format +msgid "" +"The total length of the selected entities is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"The total length of the selected entities is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." + +#: solvespace.cpp:1006 +msgid "Bad selection for perimeter; select line segments, arcs, and curves." +msgstr "Bad selection for perimeter; select line segments, arcs, and curves." + +#: solvespace.cpp:1022 +msgid "Bad selection for trace; select a single point." +msgstr "Bad selection for trace; select a single point." + +#: solvespace.cpp:1049 +#, c-format +msgid "Couldn't write to '%s'" +msgstr "Couldn't write to '%s'" + +#: solvespace.cpp:1079 +msgid "The mesh is self-intersecting (NOT okay, invalid)." +msgstr "The mesh is self-intersecting (NOT okay, invalid)." + +#: solvespace.cpp:1080 +msgid "The mesh is not self-intersecting (okay, valid)." +msgstr "The mesh is not self-intersecting (okay, valid)." + +#: solvespace.cpp:1082 +msgid "The mesh has naked edges (NOT okay, invalid)." +msgstr "The mesh has naked edges (NOT okay, invalid)." + +#: solvespace.cpp:1083 +msgid "The mesh is watertight (okay, valid)." +msgstr "The mesh is watertight (okay, valid)." + +#: solvespace.cpp:1086 +#, c-format +msgid "" +"\n" +"\n" +"The model contains %d triangles, from %d surfaces." +msgstr "" +"\n" +"\n" +"The model contains %d triangles, from %d surfaces." + +#: solvespace.cpp:1090 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"Zero problematic edges, good.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"Zero problematic edges, good.%s" + +#: solvespace.cpp:1093 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d problematic edges, bad.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d problematic edges, bad.%s" + +#: solvespace.cpp:1106 +#, c-format +msgid "" +"This is SolveSpace version %s.\n" +"\n" +"For more information, see http://solvespace.com/\n" +"\n" +"SolveSpace is free software: you are free to modify\n" +"and/or redistribute it under the terms of the GNU\n" +"General Public License (GPL) version 3 or later.\n" +"\n" +"There is NO WARRANTY, to the extent permitted by\n" +"law. For details, visit http://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues and other authors.\n" +msgstr "" +"This is SolveSpace version %s.\n" +"\n" +"For more information, see http://solvespace.com/\n" +"\n" +"SolveSpace is free software: you are free to modify\n" +"and/or redistribute it under the terms of the GNU\n" +"General Public License (GPL) version 3 or later.\n" +"\n" +"There is NO WARRANTY, to the extent permitted by\n" +"law. For details, visit http://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues and other authors.\n" + +#: style.cpp:185 +msgid "" +"Can't assign style to an entity that's derived from another entity; try " +"assigning a style to this entity's parent." +msgstr "" +"Can't assign style to an entity that's derived from another entity; try " +"assigning a style to this entity's parent." + +#: style.cpp:735 +msgid "Style name cannot be empty" +msgstr "Style name cannot be empty" + +#: textscreens.cpp:837 +msgid "Can't repeat fewer than 1 time." +msgstr "Can't repeat fewer than 1 time." + +#: textscreens.cpp:841 +msgid "Can't repeat more than 999 times." +msgstr "Can't repeat more than 999 times." + +#: textscreens.cpp:866 +msgid "Group name cannot be empty" +msgstr "Group name cannot be empty" + +#: textscreens.cpp:918 +msgid "Opacity must be between zero and one." +msgstr "Opacity must be between zero and one." + +#: textscreens.cpp:953 +msgid "Radius cannot be zero or negative." +msgstr "Radius cannot be zero or negative." + +#: toolbar.cpp:18 +msgid "Sketch line segment" +msgstr "Sketch line segment" + +#: toolbar.cpp:20 +msgid "Sketch rectangle" +msgstr "Sketch rectangle" + +#: toolbar.cpp:22 +msgid "Sketch circle" +msgstr "Sketch circle" + +#: toolbar.cpp:24 +msgid "Sketch arc of a circle" +msgstr "Sketch arc of a circle" + +#: toolbar.cpp:26 +msgid "Sketch curves from text in a TrueType font" +msgstr "Sketch curves from text in a TrueType font" + +#: toolbar.cpp:28 +msgid "Sketch image from a file" +msgstr "Sketch image from a file" + +#: toolbar.cpp:30 +msgid "Create tangent arc at selected point" +msgstr "Create tangent arc at selected point" + +#: toolbar.cpp:32 +msgid "Sketch cubic Bezier spline" +msgstr "Sketch cubic Bezier spline" + +#: toolbar.cpp:34 +msgid "Sketch datum point" +msgstr "Sketch datum point" + +#: toolbar.cpp:36 +msgid "Toggle construction" +msgstr "Toggle construction" + +#: toolbar.cpp:38 +msgid "Split lines / curves where they intersect" +msgstr "Split lines / curves where they intersect" + +#: toolbar.cpp:42 +msgid "Constrain distance / diameter / length" +msgstr "Constrain distance / diameter / length" + +#: toolbar.cpp:44 +msgid "Constrain angle" +msgstr "Constrain angle" + +#: toolbar.cpp:46 +msgid "Constrain to be horizontal" +msgstr "Constrain to be horizontal" + +#: toolbar.cpp:48 +msgid "Constrain to be vertical" +msgstr "Constrain to be vertical" + +#: toolbar.cpp:50 +msgid "Constrain to be parallel or tangent" +msgstr "Constrain to be parallel or tangent" + +#: toolbar.cpp:52 +msgid "Constrain to be perpendicular" +msgstr "Constrain to be perpendicular" + +#: toolbar.cpp:54 +msgid "Constrain point on line / curve / plane / point" +msgstr "Constrain point on line / curve / plane / point" + +#: toolbar.cpp:56 +msgid "Constrain symmetric" +msgstr "Constrain symmetric" + +#: toolbar.cpp:58 +msgid "Constrain equal length / radius / angle" +msgstr "Constrain equal length / radius / angle" + +#: toolbar.cpp:60 +msgid "Constrain normals in same orientation" +msgstr "Constrain normals in same orientation" + +#: toolbar.cpp:62 +msgid "Other supplementary angle" +msgstr "Other supplementary angle" + +#: toolbar.cpp:64 +msgid "Toggle reference dimension" +msgstr "Toggle reference dimension" + +#: toolbar.cpp:68 +msgid "New group extruding active sketch" +msgstr "New group extruding active sketch" + +#: toolbar.cpp:70 +msgid "New group rotating active sketch" +msgstr "New group rotating active sketch" + +#: toolbar.cpp:72 +msgid "New group helix from active sketch" +msgstr "New group helix from active sketch" + +#: toolbar.cpp:74 +msgid "New group revolve active sketch" +msgstr "New group revolve active sketch" + +#: toolbar.cpp:76 +msgid "New group step and repeat rotating" +msgstr "New group step and repeat rotating" + +#: toolbar.cpp:78 +msgid "New group step and repeat translating" +msgstr "New group step and repeat translating" + +#: toolbar.cpp:80 +msgid "New group in new workplane (thru given entities)" +msgstr "New group in new workplane (thru given entities)" + +#: toolbar.cpp:82 +msgid "New group in 3d" +msgstr "New group in 3d" + +#: toolbar.cpp:84 +msgid "New group linking / assembling file" +msgstr "New group linking / assembling file" + +#: toolbar.cpp:88 +msgid "Nearest isometric view" +msgstr "Nearest isometric view" + +#: toolbar.cpp:90 +msgid "Align view to active workplane" +msgstr "Align view to active workplane" + +#: util.cpp:165 +msgctxt "title" +msgid "Error" +msgstr "Error" + +#: util.cpp:165 +msgctxt "title" +msgid "Message" +msgstr "Message" + +#: util.cpp:170 +msgctxt "button" +msgid "&OK" +msgstr "&OK" + +#: view.cpp:127 +msgid "Scale cannot be zero or negative." +msgstr "Scale cannot be zero or negative." + +#: view.cpp:139 view.cpp:148 +msgid "Bad format: specify x, y, z" +msgstr "Bad format: specify x, y, z" + +#~ msgid "" +#~ "The tangent arc and line segment must share an endpoint. Constrain them " +#~ "with Constrain -> On Point before constraining tangent." +#~ msgstr "" +#~ "The tangent arc and line segment must share an endpoint. Constrain them " +#~ "with Constrain -> On Point before constraining tangent." + +#~ msgid "" +#~ "The tangent cubic and line segment must share an endpoint. Constrain them " +#~ "with Constrain -> On Point before constraining tangent." +#~ msgstr "" +#~ "The tangent cubic and line segment must share an endpoint. Constrain them " +#~ "with Constrain -> On Point before constraining tangent." + +#~ msgid "" +#~ "The curves must share an endpoint. Constrain them with Constrain -> On " +#~ "Point before constraining tangent." +#~ msgstr "" +#~ "The curves must share an endpoint. Constrain them with Constrain -> On " +#~ "Point before constraining tangent." + +#~ msgid "" +#~ "Bad selection for parallel / tangent constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two faces\n" +#~ " * two or more line segments (parallel)\n" +#~ " * one or more line segments and one or more normals (parallel)\n" +#~ " * two or more normals (parallel)\n" +#~ " * two line segments, arcs, or beziers, that share an endpoint " +#~ "(tangent)\n" +#~ msgstr "" +#~ "Bad selection for parallel / tangent constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two faces\n" +#~ " * two or more line segments (parallel)\n" +#~ " * one or more line segments and one or more normals (parallel)\n" +#~ " * two or more normals (parallel)\n" +#~ " * two line segments, arcs, or beziers, that share an endpoint " +#~ "(tangent)\n" + +#~ msgid "&Workplane" +#~ msgstr "&Workplane" + +#~ msgid "&Image" +#~ msgstr "&Image" + +#~ msgid "Tangent &Arc at Point" +#~ msgstr "Tangent &Arc at Point" + +#~ msgid "" +#~ "Bad selection for on point / curve / plane constraint. This constraint " +#~ "can apply to:\n" +#~ "\n" +#~ " * two points (points coincident)\n" +#~ " * a point and a workplane (point in plane)\n" +#~ " * a point and a line segment (point on line)\n" +#~ " * a point and a circle or arc (point on curve)\n" +#~ " * a point and a plane face (point on face)\n" +#~ msgstr "" +#~ "Bad selection for on point / curve / plane constraint. This constraint " +#~ "can apply to:\n" +#~ "\n" +#~ " * two points (points coincident)\n" +#~ " * a point and a workplane (point in plane)\n" +#~ " * a point and a line segment (point on line)\n" +#~ " * a point and a circle or arc (point on curve)\n" +#~ " * a point and a plane face (point on face)\n" + +#~ msgid "" +#~ "Bad selection for equal length / radius constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two line segments (equal length)\n" +#~ " * two line segments and two points (equal point-line distances)\n" +#~ " * a line segment and two points (equal point-line distances)\n" +#~ " * a line segment, and a point and line segment (point-line distance " +#~ "equals length)\n" +#~ " * four line segments or normals (equal angle between A,B and C,D)\n" +#~ " * three line segments or normals (equal angle between A,B and B,C)\n" +#~ " * two circles or arcs (equal radius)\n" +#~ " * a line segment and an arc (line segment length equals arc length)\n" +#~ msgstr "" +#~ "Bad selection for equal length / radius constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two line segments (equal length)\n" +#~ " * two line segments and two points (equal point-line distances)\n" +#~ " * a line segment and two points (equal point-line distances)\n" +#~ " * a line segment, and a point and line segment (point-line distance " +#~ "equals length)\n" +#~ " * four line segments or normals (equal angle between A,B and C,D)\n" +#~ " * three line segments or normals (equal angle between A,B and B,C)\n" +#~ " * two circles or arcs (equal radius)\n" +#~ " * a line segment and an arc (line segment length equals arc length)\n" + +#~ msgid "" +#~ "Bad selection for horizontal / vertical constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two points\n" +#~ " * a line segment\n" +#~ msgstr "" +#~ "Bad selection for horizontal / vertical constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two points\n" +#~ " * a line segment\n" + +#~ msgid "" +#~ "Bad selection for angle constraint. This constraint can apply to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ " * a line segment and a normal\n" +#~ " * two normals\n" +#~ msgstr "" +#~ "Bad selection for angle constraint. This constraint can apply to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ " * a line segment and a normal\n" +#~ " * two normals\n" + +#~ msgid "" +#~ "Bad selection for parallel / tangent constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two line segments (parallel)\n" +#~ " * a line segment and a normal (parallel)\n" +#~ " * two normals (parallel)\n" +#~ " * two line segments, arcs, or beziers, that share an endpoint " +#~ "(tangent)\n" +#~ msgstr "" +#~ "Bad selection for parallel / tangent constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two line segments (parallel)\n" +#~ " * a line segment and a normal (parallel)\n" +#~ " * two normals (parallel)\n" +#~ " * two line segments, arcs, or beziers, that share an endpoint " +#~ "(tangent)\n" + +#~ msgid "" +#~ "Bad selection for perpendicular constraint. This constraint can apply " +#~ "to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ " * a line segment and a normal\n" +#~ " * two normals\n" +#~ msgstr "" +#~ "Bad selection for perpendicular constraint. This constraint can apply " +#~ "to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ " * a line segment and a normal\n" +#~ " * two normals\n" + +#~ msgid "A&ngle" +#~ msgstr "A&ngle" + +#~ msgid "E&qual Length / Radius / Angle" +#~ msgstr "E&qual Length / Radius / Angle" + +#~ msgid "&Mirror" +#~ msgstr "&Mirror" + +#~ msgctxt "group-name" +#~ msgid "mirror" +#~ msgstr "mirror" + +#~ msgid "" +#~ "Bad selection for length ratio constraint. This constraint can apply to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ msgstr "" +#~ "Bad selection for length ratio constraint. This constraint can apply to:\n" +#~ "\n" +#~ " * two line segments\n" + +#~ msgid "" +#~ "Bad selection for length difference constraint. This constraint can apply " +#~ "to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ msgstr "" +#~ "Bad selection for length difference constraint. This constraint can apply " +#~ "to:\n" +#~ "\n" +#~ " * two line segments\n" + +#~ msgid "Length Ra&tio" +#~ msgstr "Length Ra&tio" + +#~ msgid "Length Diff&erence" +#~ msgstr "Length Diff&erence" + +#~ msgid "" +#~ "Bad selection for new sketch in workplane. This group can be created " +#~ "with:\n" +#~ "\n" +#~ " * a point (through the point, orthogonal to coordinate axes)\n" +#~ " * a point and two line segments (through the point, parallel to the " +#~ "lines)\n" +#~ " * a workplane (copy of the workplane)\n" +#~ msgstr "" +#~ "Bad selection for new sketch in workplane. This group can be created " +#~ "with:\n" +#~ "\n" +#~ " * a point (through the point, orthogonal to coordinate axes)\n" +#~ " * a point and two line segments (through the point, parallel to the " +#~ "lines)\n" +#~ " * a workplane (copy of the workplane)\n" + +#~ msgctxt "file-type" +#~ msgid "Q3D Object file" +#~ msgstr "Q3D Object file" diff --git a/res/locales/es_AR.po b/res/locales/es_AR.po new file mode 100644 index 0000000..28cb363 --- /dev/null +++ b/res/locales/es_AR.po @@ -0,0 +1,2319 @@ +# Spanish/Argentina translations for SolveSpace package. +# Copyright (C) 2017 the SolveSpace authors +# This file is distributed under the same license as the SolveSpace package. +# Maxi , 2021. +# +msgid "" +msgstr "" +"Project-Id-Version: SolveSpace 3.0\n" +"Report-Msgid-Bugs-To: phkahler@gmail.com\n" +"POT-Creation-Date: 2025-01-26 21:04+0200\n" +"PO-Revision-Date: 2021-09-17 \n" +"Last-Translator: Maxi Vasquez \n" +"Language-Team: AndesFreeDesign\n" +"Language: es_AR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: clipboard.cpp:314 +msgid "" +"Cut, paste, and copy work only in a workplane.\n" +"\n" +"Activate one with Sketch -> In Workplane." +msgstr "" +"Cortar, pegar y copiar funciona solo en un plano de trabajo.\n" +"\n" +"Activar uno con Croquis -> En Plano de trabajo." + +#: clipboard.cpp:331 +msgid "Clipboard is empty; nothing to paste." +msgstr "El portapapeles está vacío; nada que pegar." + +#: clipboard.cpp:378 +msgid "Number of copies to paste must be at least one." +msgstr "El número de copias para pegar debe ser al menos una." + +#: clipboard.cpp:394 textscreens.cpp:879 +msgid "Scale cannot be zero." +msgstr "La escala no puede ser cero." + +#: clipboard.cpp:436 +msgid "Select one point to define origin of rotation." +msgstr "Seleccione un punto para definir el origen de la rotación." + +#: clipboard.cpp:448 +msgid "Select two points to define translation vector." +msgstr "Seleccione dos puntos para definir el vector de traslación." + +#: clipboard.cpp:458 +msgid "" +"Transformation is identity. So all copies will be exactly on top of each " +"other." +msgstr "" +"La transformación es identidad. Entonces todas las copias estarán " +"exactamente una encima de la otra." + +#: clipboard.cpp:462 +msgid "Too many items to paste; split this into smaller pastes." +msgstr "Demasiados elementos para pegar; divida esto en partes más pequeñas." + +#: clipboard.cpp:467 +msgid "No workplane active." +msgstr "Ningún plano de trabajo activo." + +#: confscreen.cpp:410 +msgid "Bad format: specify coordinates as x, y, z" +msgstr "Formato incorrecto: especifique las coordenadas como x, y, z" + +#: confscreen.cpp:420 style.cpp:729 textscreens.cpp:910 +msgid "Bad format: specify color as r, g, b" +msgstr "Formato incorrecto: especifique color como r, g, b" + +#: confscreen.cpp:446 +msgid "" +"The perspective factor will have no effect until you enable View -> Use " +"Perspective Projection." +msgstr "" +"El factor de perspectiva no tendrá ningún efecto hasta que habilite Ver -> " +"Usar Proyección Perspectiva." + +#: confscreen.cpp:464 confscreen.cpp:474 +#, c-format +msgid "Specify between 0 and %d digits after the decimal." +msgstr "Especifique entre 0 y %d dígitos después del decimal." + +#: confscreen.cpp:486 +msgid "Export scale must not be zero!" +msgstr "¡La escala de exportación no debe ser cero!" + +#: confscreen.cpp:498 +msgid "Cutter radius offset must not be negative!" +msgstr "¡El desfase del radio de corte no debe ser negativo!" + +#: confscreen.cpp:557 +msgid "Bad value: autosave interval should be positive" +msgstr "Valor incorrecto: el intervalo de autoguardado debe ser positivo" + +#: confscreen.cpp:560 +msgid "Bad format: specify interval in integral minutes" +msgstr "Formato incorrecto: especifique el intervalo en minutos integrales" + +#: constraint.cpp:12 +msgctxt "constr-name" +msgid "pts-coincident" +msgstr "ps-coincidente" + +#: constraint.cpp:13 +msgctxt "constr-name" +msgid "pt-pt-distance" +msgstr "p-p-distancia" + +#: constraint.cpp:14 +msgctxt "constr-name" +msgid "pt-line-distance" +msgstr "p-línea-distancia" + +#: constraint.cpp:15 +msgctxt "constr-name" +msgid "pt-plane-distance" +msgstr "p-plano-distancia" + +#: constraint.cpp:16 +msgctxt "constr-name" +msgid "pt-face-distance" +msgstr "p-cara-distancia" + +#: constraint.cpp:17 +msgctxt "constr-name" +msgid "proj-pt-pt-distance" +msgstr "proy-p-p-distancia" + +#: constraint.cpp:18 +msgctxt "constr-name" +msgid "pt-in-plane" +msgstr "p-en-plano" + +#: constraint.cpp:19 +msgctxt "constr-name" +msgid "pt-on-line" +msgstr "p-sobre-línea" + +#: constraint.cpp:20 +msgctxt "constr-name" +msgid "pt-on-face" +msgstr "p-sobre-cara" + +#: constraint.cpp:21 +msgctxt "constr-name" +msgid "eq-length" +msgstr "igual-longitud" + +#: constraint.cpp:22 +msgctxt "constr-name" +msgid "eq-length-and-pt-ln-dist" +msgstr "igual-longitud-y-p-línea-dist" + +#: constraint.cpp:23 +msgctxt "constr-name" +msgid "eq-pt-line-distances" +msgstr "igual-p-línea-distancias" + +#: constraint.cpp:24 +msgctxt "constr-name" +msgid "length-ratio" +msgstr "longitud-radio" + +#: constraint.cpp:25 +msgctxt "constr-name" +msgid "arc-arc-length-ratio" +msgstr "arco-arco-longitud-relación" + +#: constraint.cpp:26 +msgctxt "constr-name" +msgid "arc-line-length-ratio" +msgstr "arco-línea-longitud-relación" + +#: constraint.cpp:27 +msgctxt "constr-name" +msgid "length-difference" +msgstr "longitud-diferencia" + +#: constraint.cpp:28 +msgctxt "constr-name" +msgid "arc-arc-len-difference" +msgstr "arco-arco-long-diferencia" + +#: constraint.cpp:29 +msgctxt "constr-name" +msgid "arc-line-len-difference" +msgstr "arco-línea-long-diferencia" + +#: constraint.cpp:30 +msgctxt "constr-name" +msgid "symmetric" +msgstr "simetría" + +#: constraint.cpp:31 +msgctxt "constr-name" +msgid "symmetric-h" +msgstr "simetría-h" + +#: constraint.cpp:32 +msgctxt "constr-name" +msgid "symmetric-v" +msgstr "simetría-v" + +#: constraint.cpp:33 +msgctxt "constr-name" +msgid "symmetric-line" +msgstr "simetría-línea" + +#: constraint.cpp:34 +msgctxt "constr-name" +msgid "at-midpoint" +msgstr "en-puntoMedio" + +#: constraint.cpp:35 +msgctxt "constr-name" +msgid "horizontal" +msgstr "horizontal" + +#: constraint.cpp:36 +msgctxt "constr-name" +msgid "vertical" +msgstr "vertical" + +#: constraint.cpp:37 +msgctxt "constr-name" +msgid "diameter" +msgstr "diámetro" + +#: constraint.cpp:38 +msgctxt "constr-name" +msgid "pt-on-circle" +msgstr "p-sobre-círculo" + +#: constraint.cpp:39 +msgctxt "constr-name" +msgid "same-orientation" +msgstr "misma-orientación" + +#: constraint.cpp:40 +msgctxt "constr-name" +msgid "angle" +msgstr "ángulo" + +#: constraint.cpp:41 +msgctxt "constr-name" +msgid "parallel" +msgstr "paralela" + +#: constraint.cpp:42 +msgctxt "constr-name" +msgid "arc-line-tangent" +msgstr "arco-línea-tangente" + +#: constraint.cpp:43 +msgctxt "constr-name" +msgid "cubic-line-tangent" +msgstr "cúbica-línea-tangente" + +#: constraint.cpp:44 +msgctxt "constr-name" +msgid "curve-curve-tangent" +msgstr "curva-curva-tangente" + +#: constraint.cpp:45 +msgctxt "constr-name" +msgid "perpendicular" +msgstr "perpendicular" + +#: constraint.cpp:46 +msgctxt "constr-name" +msgid "eq-radius" +msgstr "igual-radio" + +#: constraint.cpp:47 +msgctxt "constr-name" +msgid "eq-angle" +msgstr "igual-ángulo" + +#: constraint.cpp:48 +msgctxt "constr-name" +msgid "eq-line-len-arc-len" +msgstr "igual-línea-long-arco-long" + +#: constraint.cpp:49 +msgctxt "constr-name" +msgid "lock-where-dragged" +msgstr "fijación" + +#: constraint.cpp:50 +msgctxt "constr-name" +msgid "comment" +msgstr "comentario" + +#: constraint.cpp:151 +msgid "" +"The point you selected does not belong to the arc. The arc and line segment " +"do not share an end point.\n" +"\n" +"Select the end point of the arc at which you want it to be tangent to the " +"line." +msgstr "" + +#: constraint.cpp:158 +msgid "" +"The tangent arc and line segment must share an endpoint. Constrain them with " +"Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the arc at which you want it to be " +"tangent to the line." +msgstr "" +"El arco tangente y la línea deben compartir un punto final. Restringirlos " +"con Restringir -> En el punto antes de restringir la tangente.\n" +"\n" +"Alternatively select the end point of the arc at which you want it to be " +"tangent to the line." + +#: constraint.cpp:186 +msgid "" +"The point you selected is not an end point of the cubic spline. The spline " +"and line segment do not share an end point.\n" +"\n" +"Select the end point of the spline at which you want it to be tangent to the " +"line." +msgstr "" + +#: constraint.cpp:193 +msgid "" +"The tangent cubic spline and line segment must share an endpoint. Constrain " +"them with Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the cubic spline at which you want it " +"to be tangent to the line." +msgstr "" +"La tangente cúbica y la línea deben compartir un punto final. Restringirlos " +"con Restringir -> En el punto antes de restringir la tangente.\n" +"\n" +"Alternatively select the end point of the cubic spline at which you want it " +"to be tangent to the line." + +#: constraint.cpp:237 +msgid "" +"The points you selected are not end points of the two curves. The curves do " +"not share an end point.\n" +"\n" +"Select the end points of both curves at which you want them to be tangent to " +"each other." +msgstr "" + +#: constraint.cpp:244 +msgid "" +"The curves must share an endpoint. Constrain them with Constrain -> On Point " +"before constraining tangent.\n" +"\n" +"Alternatively select the end points of both curves at which you want the " +"curves to be tangent." +msgstr "" +"Las curvas deben compartir un punto final. Restringirlos con Restringir -> " +"En el punto antes de restringir la tangente.\n" +"\n" +"Alternatively select the end points of both curves at which you want the " +"curves to be tangent." + +#: constraint.cpp:303 +msgid "" +"Bad selection for distance / diameter constraint. This constraint can apply " +"to:\n" +"\n" +" * two points (distance between points)\n" +" * a line segment (length)\n" +" * two points and a line segment or normal (projected distance)\n" +" * a workplane and a point (minimum distance)\n" +" * a line segment and a point (minimum distance)\n" +" * a plane face and a point (minimum distance)\n" +" * a circle or an arc (diameter)\n" +msgstr "" +"Selección incorrecta para la restricción de distancia / diámetro. Esta " +"restricción puede aplicarse a:\n" +"\n" +" * dos puntos (distancia entre puntos)\n" +" * un segmento de línea (longitud)\n" +" * dos puntos y un segmento de línea o normal (distancia proyectada)\n" +" * un plano de trabajo y un punto (distancia mínima)\n" +" * un segmento de línea y un punto (distancia mínima)\n" +" * una cara plana y un punto (distancia mínima)\n" +" * un círculo o un arco (diámetro)\n" + +#: constraint.cpp:366 +msgid "" +"Bad selection for on point / curve / plane constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points (points coincident)\n" +" * a point and a workplane (point in plane)\n" +" * a point and a line segment (point on line)\n" +" * a point and a circle or arc (point on curve)\n" +" * a point and one to three plane faces (point on face(s))\n" +msgstr "" +"Selección incorrecta para la restricción de punto / curva / plano. Esta " +"restricción puede aplicarse a:\n" +"\n" +" * dos o más puntos (puntos coincidentes) \n" +" * un punto y un plano de trabajo (punto en el plano) \n" +" * un punto y un segmento de línea (punto en la línea) \n" +" * un punto y un círculo o arco (punto en la curva) \n" +" * un punto y de una a tres caras planas (punto en la(s) cara(s)) \n" + +#: constraint.cpp:427 +msgid "" +"Bad selection for equal length / radius constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more line segments (equal length)\n" +" * two line segments and two points (equal point-line distances)\n" +" * a line segment and two points (equal point-line distances)\n" +" * a line segment, and a point and line segment (point-line distance " +"equals length)\n" +" * two or more circles or arcs (equal radius)\n" +" * a line segment and an arc (line segment length equals arc length)\n" +msgstr "" +"Selección incorrecta para la restricción de igualdad longitud / radio. Esta " +"restricción puede aplicarse a:\n" +"\n" +" * dos o más segmentos de línea (igual longitud)\n" +" * dos segmentos de línea y dos puntos (distancias de línea-punto " +"iguales)\n" +" * un segmento de línea y dos puntos (distancias punto-línea iguales)\n" +" * un segmento de línea, y un punto y un segmento de línea (distancia " +"punto-línea igual a la longitud)\n" +" * dos o más círculos o arcos (igual radio)\n" +" * un segmento de línea y un arco (la longitud del segmento de línea es " +"igual a la longitud del arco)\n" + +#: constraint.cpp:480 +msgid "" +"Bad selection for length ratio constraint. This constraint can apply to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"Selección incorrecta por restricción de la relación de longitud. Esta " +"restricción puede aplicarse a:\n" +"\n" +" * dos segmentos de línea\n" +" * dos arcos\n" +" * un arco y un segmento de línea\n" + +#: constraint.cpp:515 +msgid "" +"Bad selection for length difference constraint. This constraint can apply " +"to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"Selección incorrecta para restricción de diferencia de longitud. Esta " +"restricción puede aplicarse a:\n" +"\n" +" * dos segmentos de línea\n" +" * dos arcos\n" +" * un arco y un segmento de línea\n" + +#: constraint.cpp:550 +msgid "" +"Bad selection for at midpoint constraint. This constraint can apply to:\n" +"\n" +" * a line segment and a point (point at midpoint)\n" +" * a line segment and a workplane (line's midpoint on plane)\n" +msgstr "" +"Selección incorrecta para una restricción de punto medio. Esta restricción " +"se puede aplicar a:\n" +"\n" +" * un segmento de línea y un punto (punto en el punto medio)\n" +" * un segmento de línea y un plano de trabajo (punto medio de la línea en " +"el plano)\n" + +#: constraint.cpp:608 +msgid "" +"Bad selection for symmetric constraint. This constraint can apply to:\n" +"\n" +" * two points or a line segment (symmetric about workplane's coordinate " +"axis)\n" +" * line segment, and two points or a line segment (symmetric about line " +"segment)\n" +" * workplane, and two points or a line segment (symmetric about " +"workplane)\n" +msgstr "" +"Selección incorrecta para restricción de simetría. Esta restricción puede " +"aplicarse a:\n" +"\n" +" * dos puntos o un segmento de línea (simétrico con respecto al eje de " +"coordenadas del plano de trabajo)\n" +" * segmento de línea, y dos puntos o un segmento de línea (simétrico con " +"respecto al segmento de línea)\n" +" * plano de trabajo, y dos puntos o un segmento de recta (simétrico sobre " +"plano de trabajo)\n" + +#: constraint.cpp:623 +msgid "" +"A workplane must be active when constraining symmetric without an explicit " +"symmetry plane." +msgstr "" +"Un plano de trabajo debe estar activo al restringirse simétrico sin un plano " +"de simetría explícito." + +#: constraint.cpp:663 +msgid "" +"Activate a workplane (with Sketch -> In Workplane) before applying a " +"horizontal or vertical constraint." +msgstr "" +"Active un plano de trabajo (con Croquis -> En Plano de trabajo) antes de " +"aplicar una restricción horizontal o vertical." + +#: constraint.cpp:679 +msgid "" +"Bad selection for horizontal / vertical constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points\n" +" * one or more line segments\n" +msgstr "" +"Selección incorrecta por restricción horizontal / vertical. Esta restricción " +"puede aplicarse a:\n" +"\n" +" * dos o más puntos\n" +" * uno o más segmentos de línea\n" + +#: constraint.cpp:697 +msgid "" +"Bad selection for same orientation constraint. This constraint can apply " +"to:\n" +"\n" +" * two normals\n" +msgstr "" +"Selección incorrecta para la misma restricción de orientación. Esta " +"restricción puede aplicarse a:\n" +"\n" +" * dos normales\n" + +#: constraint.cpp:748 +msgid "Must select an angle constraint." +msgstr "Debe seleccionar una restricción de ángulo." + +#: constraint.cpp:761 +msgid "Must select a constraint with associated label." +msgstr "Debe seleccionar una restricción con etiqueta asociada." + +#: constraint.cpp:784 +msgid "" +"Bad selection for angle constraint. This constraint can apply to:\n" +"\n" +"Angle between:\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +"\n" +"Equal angles:\n" +" * four line segments or normals (equal angle between A,B and C,D)\n" +" * three line segments or normals (equal angle between A,B and B,C)\n" +msgstr "" +"Selección incorrecta por restricción de ángulo. Esta restricción se puede " +"aplicarse a:\n" +"\n" +"Ángulo entre:\n" +" * dos segmentos de línea\n" +" * un segmento de linea y una normal\n" +" * dos normales\n" +"\n" +"Ángulos iguales:\n" +" * cuatro segmentos de línea o normales (ángulo igual entre A,B y C,D)\n" +" * tres segmentos de línea o normales (ángulo igual entre A,B y B,C)\n" + +#: constraint.cpp:872 +msgid "Curve-curve tangency must apply in workplane." +msgstr "La tangencia curva-curva debe aplicarse en el plano de trabajo." + +#: constraint.cpp:887 +msgid "" +"Bad selection for parallel / tangent constraint. This constraint can apply " +"to:\n" +"\n" +" * two faces\n" +" * two or more line segments (parallel)\n" +" * one or more line segments and one or more normals (parallel)\n" +" * two or more normals (parallel)\n" +" * two line segments, arcs, or beziers, that share an endpoint (tangent)\n" +" * two line segments, arcs, or beziers, that do not share an endpoint and " +"the end point(s) of the curve(s) (tangent)\n" +msgstr "" +"Mala selección para restricción de paralelo / tangente. Esta restricción " +"puede aplicarse a:\n" +"\n" +" * dos caras\n" +" * dos o más segmentos de línea (paralelos)\n" +" * uno o más segmentos de línea y una o más normales (paralelas)\n" +" * dos o más normales (paralelas)\n" +" * dos segmentos de línea, arcos, o beziers, que comparten un punto final " +"(tangente)\n" +" * dos segmentos de línea, arcos, o beziers, que no comparten un punto " +"final y el punto(s) final(es) de la(s) curva(s)\n" + +#: constraint.cpp:914 +msgid "" +"Bad selection for perpendicular constraint. This constraint can apply to:\n" +"\n" +" * two faces\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +msgstr "" +"Selección incorrecta por restricción perpendicular. Esta restricción se " +"puede aplicarse a:\n" +"\n" +" * dos caras\n" +" * dos segmentos de línea\n" +" * un segmento de línea y una normal\n" +" * dos normales\n" + +#: constraint.cpp:931 +msgid "" +"Bad selection for lock point where dragged constraint. This constraint can " +"apply to:\n" +"\n" +" * a point\n" +msgstr "" +"Selección incorrecta para el punto de bloqueo donde se arrastró la " +"restricción. Esta restricción puede aplicarse a:\n" +"\n" +" * un punto\n" + +#: constraint.cpp:946 mouse.cpp:1160 +msgid "NEW COMMENT -- DOUBLE-CLICK TO EDIT" +msgstr "NUEVO COMENTARIO -- DOBLE-CLIC PARA EDITAR" + +#: constraint.cpp:952 +msgid "click center of comment text" +msgstr "clic en el centro del texto del comentario" + +#: export.cpp:19 +msgid "" +"No solid model present; draw one with extrudes and revolves, or use Export " +"2d View to export bare lines and curves." +msgstr "" +"No hay un modelo sólido presente; dibuje uno con extrusiones y revoluciones, " +"o use Exportar Vista 2d para exportar líneas y curvas simples." + +#: export.cpp:61 +msgid "" +"Bad selection for export section. Please select:\n" +"\n" +" * nothing, with an active workplane (workplane is section plane)\n" +" * a face (section plane through face)\n" +" * a point and two line segments (plane through point and parallel to " +"lines)\n" +msgstr "" +"Selección incorrecta para la sección de exportación. Por favor seleccione:\n" +"\n" +" * nada, con un plano de trabajo activo (el plano de trabajo es un plano " +"de sección)\n" +" * una cara (plano de sección a través de la cara)\n" +" * un punto y dos segmentos de línea (plano que pasa por el punto y " +"paralelo a las líneas)\n" + +#: export.cpp:818 +msgid "Active group mesh is empty; nothing to export." +msgstr "La malla del grupo activo está vacía; nada para exportar." + +#: exportvector.cpp:336 +msgid "freehand lines were replaced with continuous lines" +msgstr "las líneas a mano alzada fueron reemplazadas por líneas continuas" + +#: exportvector.cpp:338 +msgid "zigzag lines were replaced with continuous lines" +msgstr "las líneas en zigzag fueron reemplazadas por líneas continuas" + +#: exportvector.cpp:592 +msgid "" +"Some aspects of the drawing have no DXF equivalent and were not exported:\n" +msgstr "" +"Algunos aspectos del dibujo no tienen equivalente DXF y no se exportaron:\n" + +#: exportvector.cpp:838 +msgid "" +"PDF page size exceeds 200 by 200 inches; many viewers may reject this file." +msgstr "" +"El tamaño de la página PDF supera los 5080mm x 5080mm; muchos usuarios " +"pueden rechazar este archivo." + +#: file.cpp:44 group.cpp:91 +msgctxt "group-name" +msgid "sketch-in-plane" +msgstr "croquis-en-plano" + +#: file.cpp:62 +msgctxt "group-name" +msgid "#references" +msgstr "#referencias" + +#: file.cpp:555 +msgid "The file is empty. It may be corrupt." +msgstr "El archivo esta vacío. Puede estar corrupto." + +#: file.cpp:560 +msgid "" +"Unrecognized data in file. This file may be corrupt, or from a newer version " +"of the program." +msgstr "" +"Datos no reconocidos en el archivo. Este archivo puede estar dañado o ser de " +"una versión más reciente del programa." + +#: file.cpp:876 +msgctxt "title" +msgid "Missing File" +msgstr "Archivo perdido" + +#: file.cpp:877 +#, c-format +msgctxt "dialog" +msgid "The linked file “%s” is not present." +msgstr "El archivo vinculado “%s” no esta presente." + +#: file.cpp:879 +msgctxt "dialog" +msgid "" +"Do you want to locate it manually?\n" +"\n" +"If you decline, any geometry that depends on the missing file will be " +"permanently removed." +msgstr "" +"¿Querés localizarlo manualmente?\n" +"\n" +"Si lo rechaza, cualquier geometría que dependa del archivo faltante se " +"eliminará permanentemente." + +#: file.cpp:882 +msgctxt "button" +msgid "&Yes" +msgstr "&Si" + +#: file.cpp:884 +msgctxt "button" +msgid "&No" +msgstr "&No" + +#: file.cpp:886 solvespace.cpp:652 +msgctxt "button" +msgid "&Cancel" +msgstr "&Cancelar" + +#: graphicswin.cpp:41 +msgid "&File" +msgstr "&Archivo" + +#: graphicswin.cpp:42 +msgid "&New" +msgstr "&Nuevo" + +#: graphicswin.cpp:43 +msgid "&Open..." +msgstr "&Abrir..." + +#: graphicswin.cpp:44 +msgid "Open &Recent" +msgstr "Abrir &Reciente" + +#: graphicswin.cpp:45 +msgid "&Save" +msgstr "&Guardar" + +#: graphicswin.cpp:46 +msgid "Save &As..." +msgstr "Guardar &Como..." + +#: graphicswin.cpp:48 +msgid "Export &Image..." +msgstr "Exportar &Imagen..." + +#: graphicswin.cpp:49 +msgid "Export 2d &View..." +msgstr "Exportar &Vista 2d..." + +#: graphicswin.cpp:50 +msgid "Export 2d &Section..." +msgstr "Exportar &Corte 2d..." + +#: graphicswin.cpp:51 +msgid "Export 3d &Wireframe..." +msgstr "Exportar &Estructura alámbrica 3d..." + +#: graphicswin.cpp:52 +msgid "Export Triangle &Mesh..." +msgstr "Exportar &Malla Triangular..." + +#: graphicswin.cpp:53 +msgid "Export &Surfaces..." +msgstr "Exportar &Superficies..." + +#: graphicswin.cpp:54 +msgid "Im&port..." +msgstr "Im&portar..." + +#: graphicswin.cpp:57 +msgid "E&xit" +msgstr "S&alir" + +#: graphicswin.cpp:60 +msgid "&Edit" +msgstr "&Editar" + +#: graphicswin.cpp:61 +msgid "&Undo" +msgstr "&Deshacer" + +#: graphicswin.cpp:62 +msgid "&Redo" +msgstr "&Rehacer" + +#: graphicswin.cpp:63 +msgid "Re&generate All" +msgstr "Re&generar Todo" + +#: graphicswin.cpp:65 +msgid "Snap Selection to &Grid" +msgstr "Selección Enganche a &Cuadrícula" + +#: graphicswin.cpp:66 +msgid "Rotate Imported &90°" +msgstr "Rotar Importado a &90°" + +#: graphicswin.cpp:68 +msgid "Cu&t" +msgstr "Co&rtar" + +#: graphicswin.cpp:69 +msgid "&Copy" +msgstr "&Copiar" + +#: graphicswin.cpp:70 +msgid "&Paste" +msgstr "&Pegar" + +#: graphicswin.cpp:71 +msgid "Paste &Transformed..." +msgstr "Pegar &Transformado..." + +#: graphicswin.cpp:72 +msgid "&Delete" +msgstr "&Eliminar" + +#: graphicswin.cpp:74 +msgid "Select &Edge Chain" +msgstr "Seleccionar &Cadena de Aristas" + +#: graphicswin.cpp:75 +msgid "Select &All" +msgstr "Seleccionar &Todo" + +#: graphicswin.cpp:76 +msgid "&Unselect All" +msgstr "&Deseleccionar Todo" + +#: graphicswin.cpp:78 +msgid "&Line Styles..." +msgstr "Estilos de &Línea..." + +#: graphicswin.cpp:79 +msgid "&View Projection..." +msgstr "&Ver Proyección..." + +#: graphicswin.cpp:81 +msgid "Con&figuration..." +msgstr "Con&figuración..." + +#: graphicswin.cpp:84 +msgid "&View" +msgstr "&Vista" + +#: graphicswin.cpp:85 +msgid "Zoom &In" +msgstr "Acer&car" + +#: graphicswin.cpp:86 +msgid "Zoom &Out" +msgstr "Ale&jar" + +#: graphicswin.cpp:87 +msgid "Zoom To &Fit" +msgstr "Ajustar a &Pantalla" + +#: graphicswin.cpp:89 +msgid "Align View to &Workplane" +msgstr "Alinear Vista a &Plano de trabajo" + +#: graphicswin.cpp:90 +msgid "Nearest &Ortho View" +msgstr "Vista &Ortogonal más cercana" + +#: graphicswin.cpp:91 +msgid "Nearest &Isometric View" +msgstr "Vista &Isométrica más cercana" + +#: graphicswin.cpp:92 +msgid "&Center View At Point" +msgstr "&Vista Central en Punto" + +#: graphicswin.cpp:94 +msgid "Show Snap &Grid" +msgstr "Mostrar Enganches &Cuadrícula" + +#: graphicswin.cpp:95 +msgid "Darken Inactive Solids" +msgstr "Oscurecer Sólidos Inactivos" + +#: graphicswin.cpp:96 +msgid "Use &Perspective Projection" +msgstr "Usar &Proyección Perspectiva" + +#: graphicswin.cpp:97 +msgid "Show E&xploded View" +msgstr "Mostrar Vista E&xplotada" + +#: graphicswin.cpp:98 +msgid "Dimension &Units" +msgstr "&Unidades de Cota" + +#: graphicswin.cpp:99 +msgid "Dimensions in &Millimeters" +msgstr "Cotas en &Milímetros" + +#: graphicswin.cpp:100 +msgid "Dimensions in M&eters" +msgstr "Cotas en M&etros" + +#: graphicswin.cpp:101 +msgid "Dimensions in &Inches" +msgstr "Cotas en &Pulgadas" + +#: graphicswin.cpp:102 +msgid "Dimensions in &Feet and Inches" +msgstr "Cotas en &Pies y Pulgadas" + +#: graphicswin.cpp:104 +msgid "Show &Toolbar" +msgstr "Mostrar &Barra de herramientas" + +#: graphicswin.cpp:105 +msgid "Show Property Bro&wser" +msgstr "Mostrar Navegador de Pro&piedades" + +#: graphicswin.cpp:107 +msgid "&Full Screen" +msgstr "&Pantalla Completa" + +#: graphicswin.cpp:109 +msgid "&New Group" +msgstr "&Nuevo Grupo" + +#: graphicswin.cpp:110 +msgid "Sketch In &3d" +msgstr "Croquis En &3d" + +#: graphicswin.cpp:111 +msgid "Sketch In New &Workplane" +msgstr "Croquis En Nuevo &Plano de trabajo" + +#: graphicswin.cpp:113 +msgid "Step &Translating" +msgstr "Paso &Traslación" + +#: graphicswin.cpp:114 +msgid "Step &Rotating" +msgstr "Paso &Rotación" + +#: graphicswin.cpp:116 +msgid "E&xtrude" +msgstr "E&xtrusión" + +#: graphicswin.cpp:117 +msgid "&Helix" +msgstr "&Hélice" + +#: graphicswin.cpp:118 +msgid "&Lathe" +msgstr "&Torno" + +#: graphicswin.cpp:119 +msgid "Re&volve" +msgstr "Re&volución" + +#: graphicswin.cpp:121 +msgid "Link / Assemble..." +msgstr "Enlace / Ensamblar..." + +#: graphicswin.cpp:122 +msgid "Link Recent" +msgstr "Enlace Reciente" + +#: graphicswin.cpp:124 +msgid "&Sketch" +msgstr "&Croquis" + +#: graphicswin.cpp:125 +msgid "In &Workplane" +msgstr "En &Plano de trabajo" + +#: graphicswin.cpp:126 +msgid "Anywhere In &3d" +msgstr "En cualquier lugar en &3d" + +#: graphicswin.cpp:128 +msgid "Datum &Point" +msgstr "&Punto de Referencia" + +#: graphicswin.cpp:129 +msgid "Wor&kplane" +msgstr "&Plano de trabajo" + +#: graphicswin.cpp:131 +msgid "Line &Segment" +msgstr "&Segmento de Línea" + +#: graphicswin.cpp:132 +msgid "C&onstruction Line Segment" +msgstr "S&egmento de Línea de Construcción" + +#: graphicswin.cpp:133 +msgid "&Rectangle" +msgstr "&Rectángulo" + +#: graphicswin.cpp:134 +msgid "&Circle" +msgstr "&Círculo" + +#: graphicswin.cpp:135 +msgid "&Arc of a Circle" +msgstr "&Arco de un Círculo" + +#: graphicswin.cpp:136 +msgid "&Bezier Cubic Spline" +msgstr "Spline Cúbico &Bezier" + +#: graphicswin.cpp:138 +msgid "&Text in TrueType Font" +msgstr "&Texto en Fuente TrueType" + +#: graphicswin.cpp:139 +msgid "I&mage" +msgstr "&Imagen" + +#: graphicswin.cpp:141 +msgid "To&ggle Construction" +msgstr "Al&ternar Construcción" + +#: graphicswin.cpp:142 +msgid "Ta&ngent Arc at Point" +msgstr "Tangente &Arco en el Punto" + +#: graphicswin.cpp:143 +msgid "Split Curves at &Intersection" +msgstr "Dividir Curvas en &Intersección" + +#: graphicswin.cpp:145 +msgid "&Constrain" +msgstr "&Restricción" + +#: graphicswin.cpp:146 +msgid "&Distance / Diameter" +msgstr "&Distancia / Diámetro" + +#: graphicswin.cpp:147 +msgid "Re&ference Dimension" +msgstr "Co&ta de Referencia" + +#: graphicswin.cpp:148 +msgid "A&ngle / Equal Angle" +msgstr "Á&ngulo / Igual ángulo" + +#: graphicswin.cpp:149 +msgid "Reference An&gle" +msgstr "Ángulo de Re&ferencia" + +#: graphicswin.cpp:150 +msgid "Other S&upplementary Angle" +msgstr "Otro Á&ngulo Suplementario" + +#: graphicswin.cpp:151 +msgid "Toggle R&eference Dim" +msgstr "Alternar Cota de R&eferencia" + +#: graphicswin.cpp:153 +msgid "&Horizontal" +msgstr "&Horizontal" + +#: graphicswin.cpp:154 +msgid "&Vertical" +msgstr "&Vertical" + +#: graphicswin.cpp:156 +msgid "&On Point / Curve / Plane" +msgstr "&Sobre Punto / Curva / Plano" + +#: graphicswin.cpp:157 +msgid "E&qual Length / Radius" +msgstr "I&gual Longitud / Radio" + +#: graphicswin.cpp:158 +msgid "Length / Arc Ra&tio" +msgstr "Relación Longi&tud / Arco" + +#: graphicswin.cpp:159 +msgid "Length / Arc Diff&erence" +msgstr "Dif&erencia Longitud / Arco" + +#: graphicswin.cpp:160 +msgid "At &Midpoint" +msgstr "En Punto &Medio" + +#: graphicswin.cpp:161 +msgid "S&ymmetric" +msgstr "S&imetría" + +#: graphicswin.cpp:162 +msgid "Para&llel / Tangent" +msgstr "Para&lela / Tangente" + +#: graphicswin.cpp:163 +msgid "&Perpendicular" +msgstr "&Perpendicular" + +#: graphicswin.cpp:164 +msgid "Same Orient&ation" +msgstr "Misma Orient&ación" + +#: graphicswin.cpp:165 +msgid "Lock Point Where &Dragged" +msgstr "Punto de Bloqueo &Donde se Arrastró" + +#: graphicswin.cpp:167 +msgid "Comment" +msgstr "Comentario" + +#: graphicswin.cpp:169 +msgid "&Analyze" +msgstr "&Analizar" + +#: graphicswin.cpp:170 +msgid "Measure &Volume" +msgstr "Medir &Volumen" + +#: graphicswin.cpp:171 +msgid "Measure A&rea" +msgstr "Medir Á&rea" + +#: graphicswin.cpp:172 +msgid "Measure &Perimeter" +msgstr "Medir &Perímetro" + +#: graphicswin.cpp:173 +msgid "Show &Interfering Parts" +msgstr "Mostrar &Piezas que Interfieren" + +#: graphicswin.cpp:174 +msgid "Show &Naked Edges" +msgstr "Mostrar &Aristas Visibles" + +#: graphicswin.cpp:175 +msgid "Show &Center of Mass" +msgstr "Mostrar &Centro de Masa" + +#: graphicswin.cpp:177 +msgid "Show &Underconstrained Points" +msgstr "Mostrar &Puntos Subrestringidos" + +#: graphicswin.cpp:179 +msgid "&Trace Point" +msgstr "&Punto de Rastro" + +#: graphicswin.cpp:180 +msgid "&Stop Tracing..." +msgstr "Detener ra&streo..." + +#: graphicswin.cpp:181 +msgid "Step &Dimension..." +msgstr "Paso de &Cota..." + +#: graphicswin.cpp:183 +msgid "&Help" +msgstr "&Ayuda" + +#: graphicswin.cpp:184 +msgid "&Language" +msgstr "&Idioma" + +#: graphicswin.cpp:185 +msgid "&Website / Manual" +msgstr "&Sitio Web / Manual" + +#: graphicswin.cpp:186 +msgid "&Go to GitHub commit" +msgstr "Ir a &GitHub" + +#: graphicswin.cpp:188 +msgid "&About" +msgstr "&Acerca" + +#: graphicswin.cpp:362 +msgid "(no recent files)" +msgstr "(no hay archivos recientes)" + +#: graphicswin.cpp:370 +#, c-format +msgid "File '%s' does not exist." +msgstr "El archivo '%s' no existe." + +#: graphicswin.cpp:779 +msgid "No workplane is active, so the grid will not appear." +msgstr "" +"No hay ningún plano de trabajo activo, por lo que la cuadrícula no aparecerá." + +#: graphicswin.cpp:794 +msgid "" +"The perspective factor is set to zero, so the view will always be a parallel " +"projection.\n" +"\n" +"For a perspective projection, modify the perspective factor in the " +"configuration screen. A value around 0.3 is typical." +msgstr "" +"El factor de perspectiva se establece en cero, por lo que la vista siempre " +"será una proyección paralela.\n" +"\n" +"Para una proyección en perspectiva, modifique el factor de perspectiva en la " +"pantalla de configuración. Un valor alrededor de 0.3 es típico." + +#: graphicswin.cpp:884 +msgid "" +"Select a point; this point will become the center of the view on screen." +msgstr "" +"Seleccione un punto; este punto se convertirá en el centro de la vista en " +"pantalla." + +#: graphicswin.cpp:1193 +msgid "No additional entities share endpoints with the selected entities." +msgstr "" +"Ninguna entidad adicional comparte puntos finales con las entidades " +"seleccionadas." + +#: graphicswin.cpp:1211 +msgid "" +"To use this command, select a point or other entity from an linked part, or " +"make a link group the active group." +msgstr "" +"Para usar este comando, seleccione un punto u otra entidad de una pieza " +"vinculada, o convertir un grupo de enlaces en el grupo activo." + +#: graphicswin.cpp:1234 +msgid "" +"No workplane is active. Activate a workplane (with Sketch -> In Workplane) " +"to define the plane for the snap grid." +msgstr "" +"No hay ningún plano de trabajo activo. Activar un plano de trabajo (con " +"Croquis -> En plano de trabajo) para definir el plano para el enganche a " +"cuadrícula." + +#: graphicswin.cpp:1241 +msgid "" +"Can't snap these items to grid; select points, text comments, or constraints " +"with a label. To snap a line, select its endpoints." +msgstr "" +"No se pueden enganchar estos elementos a la cuadrícula; seleccionar puntos, " +"comentarios de texto o restricciones con una etiqueta. Para enganchar una " +"línea, seleccione sus puntos finales." + +#: graphicswin.cpp:1326 +msgid "No workplane selected. Activating default workplane for this group." +msgstr "" +"No se seleccionó ningún plano de trabajo. Activando el plano de trabajo " +"predeterminado para este grupo." + +#: graphicswin.cpp:1329 +msgid "" +"No workplane is selected, and the active group does not have a default " +"workplane. Try selecting a workplane, or activating a sketch-in-new-" +"workplane group." +msgstr "" +"No se selecciona ningún plano de trabajo y el grupo activo no tiene un plano " +"de trabajo. Intente seleccionar un plano de trabajo o activar un croquis-en-" +"nuevo-grupo de plano de trabajo." + +#: graphicswin.cpp:1350 +msgid "" +"Bad selection for tangent arc at point. Select a single point, or select " +"nothing to set up arc parameters." +msgstr "" +"Selección incorrecta de arco tangente en el punto. Seleccione un solo punto " +"o no seleccione nada para configurar los parámetros del arco." + +#: graphicswin.cpp:1361 +msgid "click point on arc (draws anti-clockwise)" +msgstr "clic en el punto en el arco (dibuja en sentido antihorario)" + +#: graphicswin.cpp:1362 +msgid "click to place datum point" +msgstr "clic para colocar el punto de referencia" + +#: graphicswin.cpp:1363 +msgid "click first point of line segment" +msgstr "clic en el primer punto del segmento de línea" + +#: graphicswin.cpp:1365 +msgid "click first point of construction line segment" +msgstr "clic en el primer punto del segmento de línea de construcción" + +#: graphicswin.cpp:1366 +msgid "click first point of cubic segment" +msgstr "clic en el primer punto del segmento cúbico" + +#: graphicswin.cpp:1367 +msgid "click center of circle" +msgstr "clic en el centro del círculo" + +#: graphicswin.cpp:1368 +msgid "click origin of workplane" +msgstr "clic en origen del plano de trabajo" + +#: graphicswin.cpp:1369 +msgid "click one corner of rectangle" +msgstr "clic en una esquina del rectángulo" + +#: graphicswin.cpp:1370 +msgid "click top left of text" +msgstr "clic en la parte superior izquierda del texto" + +#: graphicswin.cpp:1376 +msgid "click top left of image" +msgstr "clic en la parte superior izquierda de la imagen" + +#: graphicswin.cpp:1402 +msgid "" +"No entities are selected. Select entities before trying to toggle their " +"construction state." +msgstr "" +"No se seleccionaron entidades. Seleccione entidades antes de intentar " +"alternar su estado de construcción." + +#: group.cpp:86 +msgctxt "group-name" +msgid "sketch-in-3d" +msgstr "croquis-en-3d" + +#: group.cpp:154 +msgid "" +"Bad selection for new sketch in workplane. This group can be created with:\n" +"\n" +" * a point (through the point, orthogonal to coordinate axes)\n" +" * a point and two line segments (through the point, parallel to the " +"lines)\n" +" * a point and a normal (through the point, orthogonal to the normal)\n" +" * a workplane (copy of the workplane)\n" +msgstr "" +"Selección incorrecta para un nuevo croquis en el plano de trabajo. Este " +"grupo se puede crear con:\n" +"\n" +" * un punto (a través del punto, ortogonal a los ejes de coordenadas)\n" +" * un punto y dos segmentos de línea (a través del punto, paralelo a " +"líneas)\n" +" * un punto y una normal (a través del punto, ortogonal a la normal)\n" +" * un plano de trabajo (copia de un plano de trabajo)\n" + +#: group.cpp:170 +msgid "" +"Activate a workplane (Sketch -> In Workplane) before extruding. The sketch " +"will be extruded normal to the workplane." +msgstr "" +"Active un plano de trabajo (Croquis -> En plano de trabajo) antes de " +"extruir. El croquis se extruirá normal al plano de trabajo." + +#: group.cpp:179 +msgctxt "group-name" +msgid "extrude" +msgstr "extrusión" + +#: group.cpp:184 +msgid "Lathe operation can only be applied to planar sketches." +msgstr "La operación de torno solo se puede aplicar a croquis planos." + +#: group.cpp:195 +msgid "" +"Bad selection for new lathe group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Selección incorrecta para el nuevo grupo de torno. Este grupo se puede crear " +"con:\n" +"\n" +" * un punto y un segmento de línea o normal (revolucionado alrededor de " +"un eje paralelo a la línea / normal, a través del punto)\n" +" * un segmento de línea (revolucionado sobre un segmento de línea)\n" + +#: group.cpp:205 +msgctxt "group-name" +msgid "lathe" +msgstr "torno" + +#: group.cpp:210 +msgid "Revolve operation can only be applied to planar sketches." +msgstr "La operación de revolución solo se puede aplicar a croquis planos." + +#: group.cpp:221 +msgid "" +"Bad selection for new revolve group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Selección incorrecta para el nuevo grupo de revolución. Este grupo se puede " +"crear con:\n" +"\n" +" * un punto y un segmento de línea o normal (revolucionado alrededor de " +"un eje paralelo a la línea / normal, a través del punto)\n" +" * un segmento de línea (revolucionado sobre un segmento de línea)\n" + +#: group.cpp:233 +msgctxt "group-name" +msgid "revolve" +msgstr "revolución" + +#: group.cpp:238 +msgid "Helix operation can only be applied to planar sketches." +msgstr "La operación de hélice solo se puede aplicar a croquis planos." + +#: group.cpp:249 +msgid "" +"Bad selection for new helix group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Selección incorrecta para el nuevo grupo de hélice. Este grupo se puede " +"crear con:\n" +"\n" +" * un punto y un segmento de línea o normal (girado alrededor de un eje " +"paralelo a la línea / normal, a través del punto)\n" +" * un segmento de línea (girado sobre un segmento de línea)\n" + +#: group.cpp:261 +msgctxt "group-name" +msgid "helix" +msgstr "hélice" + +#: group.cpp:274 +msgid "" +"Bad selection for new rotation. This group can be created with:\n" +"\n" +" * a point, while locked in workplane (rotate in plane, about that " +"point)\n" +" * a point and a line or a normal (rotate about an axis through the " +"point, and parallel to line / normal)\n" +msgstr "" +"Selección incorrecta para nueva rotación. Este grupo se puede crear con:\n" +"\n" +" * un punto, mientras está bloqueado en el plano de trabajo (girar en el " +"plano, sobre ese punto)\n" +" * un punto y una línea o una normal (rotar alrededor de un eje a través " +"de un punto y paralela a la línea / normal)\n" + +#: group.cpp:287 +msgctxt "group-name" +msgid "rotate" +msgstr "rotación" + +#: group.cpp:298 +msgctxt "group-name" +msgid "translate" +msgstr "traslación" + +#: group.cpp:422 +msgid "(unnamed)" +msgstr "(sin nombre)" + +#: groupmesh.cpp:710 +msgid "not closed contour, or not all same style!" +msgstr "¡Contorno no cerrado, o no todos del mismo estilo!" + +#: groupmesh.cpp:723 +msgid "points not all coplanar!" +msgstr "¡No todos los puntos son coplanares!" + +#: groupmesh.cpp:725 +msgid "contour is self-intersecting!" +msgstr "¡El contorno se intersecta a sí mismo!" + +#: groupmesh.cpp:727 +msgid "zero-length edge!" +msgstr "¡arista de longitud cero!" + +#: importmesh.cpp:136 +msgid "Text-formated STL files are not currently supported" +msgstr "Los archivos STL con formato de texto no son compatibles actualmente" + +#: modify.cpp:252 +msgid "Must be sketching in workplane to create tangent arc." +msgstr "" +"Debe estar dibujando en el plano de trabajo para crear un arco tangente." + +#: modify.cpp:299 +msgid "" +"To create a tangent arc, select a point where two non-construction lines or " +"circles in this group and workplane join." +msgstr "" +"Para crear un arco tangente, seleccione un punto donde dos líneas que no " +"sean de construcción o los círculos de este grupo y el plano de trabajo se " +"unen." + +#: modify.cpp:386 +msgid "" +"Couldn't round this corner. Try a smaller radius, or try creating the " +"desired geometry by hand with tangency constraints." +msgstr "" +"No pude redondear esta esquina. Pruebe con un radio más pequeño o intente " +"crear la geometría deseada a mano con restricciones de tangencia." + +#: modify.cpp:595 +msgid "Couldn't split this entity; lines, circles, or cubics only." +msgstr "No se pudo dividir esta entidad; solo líneas, círculos o cúbicos." + +#: modify.cpp:622 +msgid "Must be sketching in workplane to split." +msgstr "Debe estar dibujando en el plano de trabajo para dividir." + +#: modify.cpp:629 +msgid "" +"Select two entities that intersect each other (e.g. two lines/circles/arcs " +"or a line/circle/arc and a point)." +msgstr "" +"Seleccione dos entidades que se crucen entre sí (por ejemplo, dos líneas/" +"círculos/arcos o una línea/círculo/arco y un punto)." + +#: modify.cpp:734 +msgid "Can't split; no intersection found." +msgstr "No se puede dividir; no se encontró ninguna intersección." + +#: mouse.cpp:558 +msgid "Assign to Style" +msgstr "Asignar a Estilo" + +#: mouse.cpp:574 +msgid "No Style" +msgstr "Sin Estilo" + +#: mouse.cpp:577 +msgid "Newly Created Custom Style..." +msgstr "Estilo Personalizado Recién Creado..." + +#: mouse.cpp:584 +msgid "Group Info" +msgstr "Información de Grupo" + +#: mouse.cpp:604 +msgid "Style Info" +msgstr "Información de Estilo" + +#: mouse.cpp:624 +msgid "Select Edge Chain" +msgstr "Seleccionar Cadena de Arista" + +#: mouse.cpp:630 +msgid "Toggle Reference Dimension" +msgstr "Alternar Cota de Referencia" + +#: mouse.cpp:636 +msgid "Other Supplementary Angle" +msgstr "Otro Ángulo Suplementario" + +#: mouse.cpp:641 +msgid "Snap to Grid" +msgstr "Enganchar a la cuadrícula" + +#: mouse.cpp:650 +msgid "Remove Spline Point" +msgstr "Remover Punto de Spline" + +#: mouse.cpp:685 +msgid "Add Spline Point" +msgstr "Agregar Punto de Spline" + +#: mouse.cpp:689 +msgid "Cannot add spline point: maximum number of points reached." +msgstr "" +"No se puede agregar un punto de spline: se alcanzó el número máximo de " +"puntos." + +#: mouse.cpp:714 +msgid "Toggle Construction" +msgstr "Alternar Construcción" + +#: mouse.cpp:730 +msgid "Delete Point-Coincident Constraint" +msgstr "Eliminar Restricción de Punto-Coincidente" + +#: mouse.cpp:748 +msgid "Cut" +msgstr "Cortar" + +#: mouse.cpp:750 +msgid "Copy" +msgstr "Copiar" + +#: mouse.cpp:754 +msgid "Select All" +msgstr "Seleccionar Todo" + +#: mouse.cpp:759 +msgid "Paste" +msgstr "Pegar" + +#: mouse.cpp:761 +msgid "Paste Transformed..." +msgstr "Pegar Transformado..." + +#: mouse.cpp:766 +msgid "Delete" +msgstr "Eliminar" + +#: mouse.cpp:769 +msgid "Unselect All" +msgstr "Deseleccionar Todo" + +#: mouse.cpp:776 +msgid "Unselect Hovered" +msgstr "Deselección Flotante" + +#: mouse.cpp:785 +msgid "Zoom to Fit" +msgstr "Ajustar a Pantalla" + +#: mouse.cpp:987 mouse.cpp:1276 +msgid "click next point of line, or press Esc" +msgstr "clic en el siguiente punto de la línea o presione Esc" + +#: mouse.cpp:993 +msgid "" +"Can't draw rectangle in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"No se puede dibujar un rectángulo en 3d; primero, active un plano de trabajo " +"con Croquis -> En Plano de trabajo." + +#: mouse.cpp:1027 +msgid "click to place other corner of rectangle" +msgstr "clic para colocar la otra esquina del rectángulo" + +#: mouse.cpp:1048 +msgid "click to set radius" +msgstr "clic para establecer el radio" + +#: mouse.cpp:1053 +msgid "" +"Can't draw arc in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"No se puede dibujar un arco en 3d; primero, active un plano de trabajo con " +"Croquis -> En Plano de trabajo." + +#: mouse.cpp:1072 +msgid "click to place point" +msgstr "clic para colocar el punto" + +#: mouse.cpp:1088 +msgid "click next point of cubic, or press Esc" +msgstr "clic en el siguiente punto del cúbico, o presione Esc" + +#: mouse.cpp:1093 +msgid "" +"Sketching in a workplane already; sketch in 3d before creating new workplane." +msgstr "" +"Croquizando en un plano de trabajo ya; croquis en 3d antes de crear un nuevo " +"plano de trabajo." + +#: mouse.cpp:1109 +msgid "" +"Can't draw text in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"No se puede dibujar texto en 3D; primero, active un plano de trabajo con " +"Croquis -> En Plano de trabajo." + +#: mouse.cpp:1126 +msgid "click to place bottom right of text" +msgstr "clic para colocar la parte inferior derecha del texto" + +#: mouse.cpp:1132 +msgid "" +"Can't draw image in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"No se puede dibujar una imagen en 3D; primero, active un plano de trabajo " +"con Croquis -> En Plano de Trabajo." + +#: platform/gui.cpp:85 platform/gui.cpp:90 solvespace.cpp:583 +msgctxt "file-type" +msgid "SolveSpace models" +msgstr "Modelos SolveSpace" + +#: platform/gui.cpp:89 +msgctxt "file-type" +msgid "ALL" +msgstr "TODO" + +#: platform/gui.cpp:91 +msgctxt "file-type" +msgid "IDF circuit board" +msgstr "Placa circuito IDF" + +#: platform/gui.cpp:92 +msgctxt "file-type" +msgid "STL triangle mesh" +msgstr "Malla de triángulo STL" + +#: platform/gui.cpp:96 +msgctxt "file-type" +msgid "PNG image" +msgstr "Imagen PNG" + +#: platform/gui.cpp:100 +msgctxt "file-type" +msgid "STL mesh" +msgstr "Malla STL" + +#: platform/gui.cpp:101 +msgctxt "file-type" +msgid "Wavefront OBJ mesh" +msgstr "Malla Wavefront OBJ" + +#: platform/gui.cpp:102 +msgctxt "file-type" +msgid "Three.js-compatible mesh, with viewer" +msgstr "Malla compatible-Three.js, con visor" + +#: platform/gui.cpp:103 +msgctxt "file-type" +msgid "Three.js-compatible mesh, mesh only" +msgstr "Malla compatible-Three.js, solo malla" + +#: platform/gui.cpp:104 +msgctxt "file-type" +msgid "VRML text file" +msgstr "Archivo texto VRML" + +#: platform/gui.cpp:108 platform/gui.cpp:115 platform/gui.cpp:122 +msgctxt "file-type" +msgid "STEP file" +msgstr "Archivo STEP" + +#: platform/gui.cpp:112 +msgctxt "file-type" +msgid "PDF file" +msgstr "Archivo PDF" + +#: platform/gui.cpp:113 +msgctxt "file-type" +msgid "Encapsulated PostScript" +msgstr "PostScript Encapsulado" + +#: platform/gui.cpp:114 +msgctxt "file-type" +msgid "Scalable Vector Graphics" +msgstr "Gráficos Vectoriales Escalables SVG" + +#: platform/gui.cpp:116 platform/gui.cpp:123 +msgctxt "file-type" +msgid "DXF file (AutoCAD 2007)" +msgstr "Archivo DXF (AutoCAD 2007)" + +#: platform/gui.cpp:117 +msgctxt "file-type" +msgid "HPGL file" +msgstr "Archivo HPGL" + +#: platform/gui.cpp:118 +msgctxt "file-type" +msgid "G Code" +msgstr "G Code" + +#: platform/gui.cpp:127 +msgctxt "file-type" +msgid "AutoCAD DXF and DWG files" +msgstr "Archivos AutoCAD DXF y DWG" + +#: platform/gui.cpp:131 +msgctxt "file-type" +msgid "Comma-separated values" +msgstr "Valores separados por comas" + +#: platform/guigtk.cpp:1434 platform/guimac.mm:1513 platform/guiwin.cpp:1641 +msgid "untitled" +msgstr "sin título" + +#: platform/guigtk.cpp:1445 platform/guigtk.cpp:1481 platform/guimac.mm:1471 +#: platform/guiwin.cpp:1639 +msgctxt "title" +msgid "Save File" +msgstr "Guardar Archivo" + +#: platform/guigtk.cpp:1446 platform/guigtk.cpp:1482 platform/guimac.mm:1454 +#: platform/guiwin.cpp:1645 +msgctxt "title" +msgid "Open File" +msgstr "Abrir Archivo" + +#: platform/guigtk.cpp:1449 platform/guigtk.cpp:1488 +msgctxt "button" +msgid "_Cancel" +msgstr "_Cancelar" + +#: platform/guigtk.cpp:1450 platform/guigtk.cpp:1486 +msgctxt "button" +msgid "_Save" +msgstr "_Guardar" + +#: platform/guigtk.cpp:1451 platform/guigtk.cpp:1487 +msgctxt "button" +msgid "_Open" +msgstr "_Abrir" + +#: solvespace.cpp:175 +msgctxt "title" +msgid "Autosave Available" +msgstr "Autoguardado Disponible" + +#: solvespace.cpp:176 +msgctxt "dialog" +msgid "An autosave file is available for this sketch." +msgstr "Un archivo de autoguardado está disponible para este croquis." + +#: solvespace.cpp:177 +msgctxt "dialog" +msgid "Do you want to load the autosave file instead?" +msgstr "¿Desea cargar el archivo de autoguardado en su lugar?" + +#: solvespace.cpp:178 +msgctxt "button" +msgid "&Load autosave" +msgstr "&Cargar autoguardado" + +#: solvespace.cpp:180 +msgctxt "button" +msgid "Do&n't Load" +msgstr "&No Cargar" + +#: solvespace.cpp:640 +msgctxt "title" +msgid "Modified File" +msgstr "Archivo Modificado" + +#: solvespace.cpp:642 +#, c-format +msgctxt "dialog" +msgid "Do you want to save the changes you made to the sketch “%s”?" +msgstr "¿Desea guardar los cambios que realizó en el croquis “%s”?" + +#: solvespace.cpp:645 +msgctxt "dialog" +msgid "Do you want to save the changes you made to the new sketch?" +msgstr "¿Desea guardar los cambios que realizó en el nuevo croquis?" + +#: solvespace.cpp:648 +msgctxt "dialog" +msgid "Your changes will be lost if you don't save them." +msgstr "Sus cambios se perderán si no los guarda." + +#: solvespace.cpp:649 +msgctxt "button" +msgid "&Save" +msgstr "&Guardar" + +#: solvespace.cpp:651 +msgctxt "button" +msgid "Do&n't Save" +msgstr "&No Guardar" + +#: solvespace.cpp:672 +msgctxt "title" +msgid "(new sketch)" +msgstr "(nuevo croquis)" + +#: solvespace.cpp:683 +msgctxt "title" +msgid "Property Browser" +msgstr "Explorador de Propiedades" + +#: solvespace.cpp:746 +msgid "" +"Constraints are currently shown, and will be exported in the toolpath. This " +"is probably not what you want; hide them by clicking the link at the top of " +"the text window." +msgstr "" +"Las restricciones se muestran actualmente y se exportarán en la trayectoria. " +"Probablemente esto no sea lo que querés; ocultalos haciendo clic en el " +"enlace en la parte superior de la ventana de texto." + +#: solvespace.cpp:834 +#, c-format +msgid "" +"Can't identify file type from file extension of filename '%s'; try .dxf or ." +"dwg." +msgstr "" +"No se puede identificar el tipo de archivo a partir de la extensión del " +"nombre del archivo '%s'; intente .dxf o .dwg." + +#: solvespace.cpp:886 +msgid "Constraint must have a label, and must not be a reference dimension." +msgstr "" +"La restricción debe tener una etiqueta y no debe ser una cota de referencia." + +#: solvespace.cpp:890 +msgid "Bad selection for step dimension; select a constraint." +msgstr "" +"Selección incorrecta para la cota del paso; seleccione una restricción." + +#: solvespace.cpp:914 +msgid "The assembly does not interfere, good." +msgstr "El ensamble no interfiere, bien." + +#: solvespace.cpp:930 +#, c-format +msgid "" +"The volume of the solid model is:\n" +"\n" +" %s" +msgstr "" +"El volumen del modelo sólido es:\n" +"\n" +" %s" + +#: solvespace.cpp:939 +#, c-format +msgid "" +"\n" +"The volume of current group mesh is:\n" +"\n" +" %s" +msgstr "" +"\n" +"El volumen de la malla del grupo actual es:\n" +"\n" +" %s" + +#: solvespace.cpp:944 +msgid "" +"\n" +"\n" +"Curved surfaces have been approximated as triangles.\n" +"This introduces error, typically of around 1%." +msgstr "" +"\n" +"\n" +"Las superficies curvas se han aproximado como triángulos.\n" +"Esto introduce un error, normalmente alrededor del 1%." + +#: solvespace.cpp:959 +#, c-format +msgid "" +"The surface area of the selected faces is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"El área de la superficie de las caras seleccionadas es:\n" +"\n" +" %s\n" +"\n" +"Las curvas se han aproximado como lineales por partes.\n" +"Esto introduce un error, normalmente alrededor del 1%%." + +#: solvespace.cpp:968 +msgid "" +"This group does not contain a correctly-formed 2d closed area. It is open, " +"not coplanar, or self-intersecting." +msgstr "" +"Este grupo no contiene un área cerrada 2d correctamente formada. Está " +"abierta, no coplanares, o auto-intersectantes." + +#: solvespace.cpp:980 +#, c-format +msgid "" +"The area of the region sketched in this group is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"El área de la región croquizada en este grupo es:\n" +"\n" +" %s\n" +"\n" +"Las curvas se han aproximado como lineales por partes.\n" +"Esto introduce un error, normalmente alrededor del 1%%." + +#: solvespace.cpp:1000 +#, c-format +msgid "" +"The total length of the selected entities is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"La longitud total de las entidades seleccionadas es:\n" +"\n" +" %s\n" +"\n" +"Las curvas se han aproximado como lineales por partes.\n" +"Esto introduce un error, normalmente alrededor del 1%%." + +#: solvespace.cpp:1006 +msgid "Bad selection for perimeter; select line segments, arcs, and curves." +msgstr "" +"Selección incorrecta de perímetro; seleccione segmentos de línea, arcos y " +"curvas." + +#: solvespace.cpp:1022 +msgid "Bad selection for trace; select a single point." +msgstr "Selección incorrecta de rastreo; seleccione un solo punto." + +#: solvespace.cpp:1049 +#, c-format +msgid "Couldn't write to '%s'" +msgstr "No pude escribir a '%s'" + +#: solvespace.cpp:1079 +msgid "The mesh is self-intersecting (NOT okay, invalid)." +msgstr "La malla se intersecta a si misma (NO está bien, no es válida)." + +#: solvespace.cpp:1080 +msgid "The mesh is not self-intersecting (okay, valid)." +msgstr "La malla no se intersecta (está bien, es válida)." + +#: solvespace.cpp:1082 +msgid "The mesh has naked edges (NOT okay, invalid)." +msgstr "La malla tiene bordes desnudos (NO está bien, no es válida)." + +#: solvespace.cpp:1083 +msgid "The mesh is watertight (okay, valid)." +msgstr "La malla es estanca (está bien, es válida)." + +#: solvespace.cpp:1086 +#, c-format +msgid "" +"\n" +"\n" +"The model contains %d triangles, from %d surfaces." +msgstr "" +"\n" +"\n" +"El modelo contiene %d triángulos, desde %d superficies." + +#: solvespace.cpp:1090 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"Zero problematic edges, good.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"Cero aristas problemáticas, bien.%s" + +#: solvespace.cpp:1093 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d problematic edges, bad.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d aristas problemáticas, mal.%s" + +#: solvespace.cpp:1106 +#, c-format +msgid "" +"This is SolveSpace version %s.\n" +"\n" +"For more information, see http://solvespace.com/\n" +"\n" +"SolveSpace is free software: you are free to modify\n" +"and/or redistribute it under the terms of the GNU\n" +"General Public License (GPL) version 3 or later.\n" +"\n" +"There is NO WARRANTY, to the extent permitted by\n" +"law. For details, visit http://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues and other authors.\n" +msgstr "" +"Esto es SolveSpace versión %s.\n" +"\n" +"Para más información, ver http://solvespace.com/\n" +"\n" +"SolveSpace es software libre: sos libre de modificarlo\n" +"y/o redistribuirlo bajo los términos de la GNU\n" +"Licencia Pública General (GPL) versión 3 o posterior.\n" +"\n" +"NO HAY GARANTÍA, en la medida permitida por\n" +"ley. Para detalles, visitá http://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues y otros autores.\n" + +#: style.cpp:185 +msgid "" +"Can't assign style to an entity that's derived from another entity; try " +"assigning a style to this entity's parent." +msgstr "" +"No se puede asignar estilo a una entidad derivada de otra entidad;; intenta " +"asignar un estilo al padre de esta entidad." + +#: style.cpp:735 +msgid "Style name cannot be empty" +msgstr "El nombre del estilo no puede estar vacío" + +#: textscreens.cpp:837 +msgid "Can't repeat fewer than 1 time." +msgstr "No se puede repetir menos de 1 vez." + +#: textscreens.cpp:841 +msgid "Can't repeat more than 999 times." +msgstr "No se puede repetir más de 999 veces." + +#: textscreens.cpp:866 +msgid "Group name cannot be empty" +msgstr "El nombre del grupo no puede estar vacío" + +#: textscreens.cpp:918 +msgid "Opacity must be between zero and one." +msgstr "La opacidad debe estar entre cero y uno." + +#: textscreens.cpp:953 +msgid "Radius cannot be zero or negative." +msgstr "El radio no puede ser cero o negativo." + +#: toolbar.cpp:18 +msgid "Sketch line segment" +msgstr "Croquizar segmento de línea" + +#: toolbar.cpp:20 +msgid "Sketch rectangle" +msgstr "Croquizar rectángulo" + +#: toolbar.cpp:22 +msgid "Sketch circle" +msgstr "Croquizar círculo" + +#: toolbar.cpp:24 +msgid "Sketch arc of a circle" +msgstr "Croquizar arco de un círculo" + +#: toolbar.cpp:26 +msgid "Sketch curves from text in a TrueType font" +msgstr "Croquizar curvas desde texto en fuente TrueType" + +#: toolbar.cpp:28 +msgid "Sketch image from a file" +msgstr "Croquizar imagen desde un archivo" + +#: toolbar.cpp:30 +msgid "Create tangent arc at selected point" +msgstr "Crear arco tangente en el punto seleccionado" + +#: toolbar.cpp:32 +msgid "Sketch cubic Bezier spline" +msgstr "Croquizar spline de Bezier cúbico" + +#: toolbar.cpp:34 +msgid "Sketch datum point" +msgstr "Croquizar punto de referencia" + +#: toolbar.cpp:36 +msgid "Toggle construction" +msgstr "Alternar construcción" + +#: toolbar.cpp:38 +msgid "Split lines / curves where they intersect" +msgstr "Dividir líneas / curvas donde se intersectan" + +#: toolbar.cpp:42 +msgid "Constrain distance / diameter / length" +msgstr "Restringir distancia / diámetro / longitud" + +#: toolbar.cpp:44 +msgid "Constrain angle" +msgstr "Restringir ángulo" + +#: toolbar.cpp:46 +msgid "Constrain to be horizontal" +msgstr "Restringir para ser horizontal" + +#: toolbar.cpp:48 +msgid "Constrain to be vertical" +msgstr "Restringir para ser vertical" + +#: toolbar.cpp:50 +msgid "Constrain to be parallel or tangent" +msgstr "Restringir para ser paralela o tangente" + +#: toolbar.cpp:52 +msgid "Constrain to be perpendicular" +msgstr "Restringir para ser perpendicular" + +#: toolbar.cpp:54 +msgid "Constrain point on line / curve / plane / point" +msgstr "Restringir punto sobre línea / curva / plano / punto" + +#: toolbar.cpp:56 +msgid "Constrain symmetric" +msgstr "Restringir simetría" + +#: toolbar.cpp:58 +msgid "Constrain equal length / radius / angle" +msgstr "Restringir igual longitud / radio / ángulo" + +#: toolbar.cpp:60 +msgid "Constrain normals in same orientation" +msgstr "Restringir normales en misma orientación" + +#: toolbar.cpp:62 +msgid "Other supplementary angle" +msgstr "Otro ángulo suplementario" + +#: toolbar.cpp:64 +msgid "Toggle reference dimension" +msgstr "Alternar cota de referencia" + +#: toolbar.cpp:68 +msgid "New group extruding active sketch" +msgstr "Nuevo croquis activo de grupo de extrusión" + +#: toolbar.cpp:70 +msgid "New group rotating active sketch" +msgstr "Nuevo croquis activo de grupo de rotación" + +#: toolbar.cpp:72 +msgid "New group helix from active sketch" +msgstr "Nuevo croquis activo de grupo de hélice" + +#: toolbar.cpp:74 +msgid "New group revolve active sketch" +msgstr "Nuevo croquis activo de grupo de revolución" + +#: toolbar.cpp:76 +msgid "New group step and repeat rotating" +msgstr "Nuevo paso de grupo y rotación repetida" + +#: toolbar.cpp:78 +msgid "New group step and repeat translating" +msgstr "Nuevo paso de grupo y traslación repetida" + +#: toolbar.cpp:80 +msgid "New group in new workplane (thru given entities)" +msgstr "Nuevo grupo en nuevo plano de trabajo (a través de entidades dadas)" + +#: toolbar.cpp:82 +msgid "New group in 3d" +msgstr "Nuevo grupo en 3d" + +#: toolbar.cpp:84 +msgid "New group linking / assembling file" +msgstr "Nuevo enlace de grupo / archivo de ensamble" + +#: toolbar.cpp:88 +msgid "Nearest isometric view" +msgstr "Vista isométrica más cercana" + +#: toolbar.cpp:90 +msgid "Align view to active workplane" +msgstr "Alinear vista al plano de trabajo activo" + +#: util.cpp:165 +msgctxt "title" +msgid "Error" +msgstr "Error" + +#: util.cpp:165 +msgctxt "title" +msgid "Message" +msgstr "Mensaje" + +#: util.cpp:170 +msgctxt "button" +msgid "&OK" +msgstr "&Aceptar" + +#: view.cpp:127 +msgid "Scale cannot be zero or negative." +msgstr "La escala no debe ser cero o negativa." + +#: view.cpp:139 view.cpp:148 +msgid "Bad format: specify x, y, z" +msgstr "Formato incorrecto: especifica x, y, z" diff --git a/res/locales/fr_FR.po b/res/locales/fr_FR.po new file mode 100644 index 0000000..4724dc0 --- /dev/null +++ b/res/locales/fr_FR.po @@ -0,0 +1,2641 @@ +# French translations for the SolveSpace package. +# Copyright (C) 2018 the SolveSpace authors +# This file is distributed under the same license as the SolveSpace package. +# whitequark , 2018. #zanata +msgid "" +msgstr "" +"Project-Id-Version: SolveSpace 3.0\n" +"Report-Msgid-Bugs-To: phkahler@gmail.com\n" +"POT-Creation-Date: 2025-01-26 21:04+0200\n" +"PO-Revision-Date: 2025-01-26 21:48+0100\n" +"Last-Translator: whitequark \n" +"Language-Team: none\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Poedit 3.2.2\n" + +#: clipboard.cpp:314 +msgid "" +"Cut, paste, and copy work only in a workplane.\n" +"\n" +"Activate one with Sketch -> In Workplane." +msgstr "" +"Couper, coller et copier fonctionnent uniquement dans un plan de travail.\n" +"\n" +"Activez un plan avec « Dessin -> Dans le plan de travail »." + +#: clipboard.cpp:331 +msgid "Clipboard is empty; nothing to paste." +msgstr "Presse papier vide ; rien à coller." + +#: clipboard.cpp:378 +msgid "Number of copies to paste must be at least one." +msgstr "Le nombre de copies à coller doit être d'au moins un." + +#: clipboard.cpp:394 textscreens.cpp:879 +msgid "Scale cannot be zero." +msgstr "L'échelle ne peut pas être zéro." + +#: clipboard.cpp:436 +msgid "Select one point to define origin of rotation." +msgstr "Sélectionnez un point pour définir l'origine de la rotation." + +#: clipboard.cpp:448 +msgid "Select two points to define translation vector." +msgstr "Sélectionnez deux points pour définir le vecteur de translation." + +#: clipboard.cpp:458 +msgid "" +"Transformation is identity. So all copies will be exactly on top of each " +"other." +msgstr "" +"Transformation identique. Donc, toutes les copies seront exactement les unes " +"au-dessus des autres." + +#: clipboard.cpp:462 +msgid "Too many items to paste; split this into smaller pastes." +msgstr "Trop d'éléments à coller ; divisez-les en plus petits groupes." + +#: clipboard.cpp:467 +msgid "No workplane active." +msgstr "Pas d'espace de travail actif." + +#: confscreen.cpp:410 +msgid "Bad format: specify coordinates as x, y, z" +msgstr "Mauvais format : spécifiez les coordonnées comme x, y, z" + +#: confscreen.cpp:420 style.cpp:729 textscreens.cpp:910 +msgid "Bad format: specify color as r, g, b" +msgstr "Mauvais format ; spécifiez la couleur comme r, v, b" + +#: confscreen.cpp:446 +msgid "" +"The perspective factor will have no effect until you enable View -> Use " +"Perspective Projection." +msgstr "" +"Le facteur de perspective n'aura aucun effet tant que vous n'aurez pas " +"activé « Affichage -> Utiliser la Vue en perspective »." + +#: confscreen.cpp:464 confscreen.cpp:474 +#, c-format +msgid "Specify between 0 and %d digits after the decimal." +msgstr "Spécifier entre 0 et %d chiffres après la virgule." + +#: confscreen.cpp:486 +msgid "Export scale must not be zero!" +msgstr "L'échelle d'export ne doit pas être zéro !" + +#: confscreen.cpp:498 +msgid "Cutter radius offset must not be negative!" +msgstr "Le décalage du rayon de coupe ne doit pas être négatif !" + +#: confscreen.cpp:557 +msgid "Bad value: autosave interval should be positive" +msgstr "" +"Mauvaise valeur : l'intervalle d'enregistrement automatique devrait être " +"positif" + +#: confscreen.cpp:560 +msgid "Bad format: specify interval in integral minutes" +msgstr "Mauvais format : spécifiez un nombre entier de minutes" + +#: constraint.cpp:12 +msgctxt "constr-name" +msgid "pts-coincident" +msgstr "pts-coïncidence" + +#: constraint.cpp:13 +msgctxt "constr-name" +msgid "pt-pt-distance" +msgstr "pt-pt-distance" + +#: constraint.cpp:14 +msgctxt "constr-name" +msgid "pt-line-distance" +msgstr "pt-ligne-distance" + +#: constraint.cpp:15 +msgctxt "constr-name" +msgid "pt-plane-distance" +msgstr "pt-plan-distance" + +#: constraint.cpp:16 +msgctxt "constr-name" +msgid "pt-face-distance" +msgstr "pt-face-distance" + +#: constraint.cpp:17 +msgctxt "constr-name" +msgid "proj-pt-pt-distance" +msgstr "proj-pt-pt-distance" + +#: constraint.cpp:18 +msgctxt "constr-name" +msgid "pt-in-plane" +msgstr "pt-dans-plan" + +#: constraint.cpp:19 +msgctxt "constr-name" +msgid "pt-on-line" +msgstr "pt-sur-ligne" + +#: constraint.cpp:20 +msgctxt "constr-name" +msgid "pt-on-face" +msgstr "pt-sur-face" + +#: constraint.cpp:21 +msgctxt "constr-name" +msgid "eq-length" +msgstr "eg-longueur" + +#: constraint.cpp:22 +msgctxt "constr-name" +msgid "eq-length-and-pt-ln-dist" +msgstr "eg-longueur-et-pt-dans-dist" + +#: constraint.cpp:23 +msgctxt "constr-name" +msgid "eq-pt-line-distances" +msgstr "eg-pt-ligne-distances" + +#: constraint.cpp:24 +msgctxt "constr-name" +msgid "length-ratio" +msgstr "longueur-ratio" + +#: constraint.cpp:25 +msgctxt "constr-name" +msgid "arc-arc-length-ratio" +msgstr "arc-arc-longueur-ratio" + +#: constraint.cpp:26 +msgctxt "constr-name" +msgid "arc-line-length-ratio" +msgstr "arc-ligne-longueur-ratio" + +#: constraint.cpp:27 +msgctxt "constr-name" +msgid "length-difference" +msgstr "longueur-difference" + +#: constraint.cpp:28 +msgctxt "constr-name" +msgid "arc-arc-len-difference" +msgstr "arc-arc-longueur-différence" + +#: constraint.cpp:29 +msgctxt "constr-name" +msgid "arc-line-len-difference" +msgstr "arc-ligne-longueur-différence" + +#: constraint.cpp:30 +msgctxt "constr-name" +msgid "symmetric" +msgstr "symétrique" + +#: constraint.cpp:31 +msgctxt "constr-name" +msgid "symmetric-h" +msgstr "symétrique-h" + +#: constraint.cpp:32 +msgctxt "constr-name" +msgid "symmetric-v" +msgstr "symétrique-v" + +#: constraint.cpp:33 +msgctxt "constr-name" +msgid "symmetric-line" +msgstr "symétrique-ligne" + +#: constraint.cpp:34 +msgctxt "constr-name" +msgid "at-midpoint" +msgstr "au-point-milieu" + +#: constraint.cpp:35 +msgctxt "constr-name" +msgid "horizontal" +msgstr "horizontal" + +#: constraint.cpp:36 +msgctxt "constr-name" +msgid "vertical" +msgstr "vertical" + +#: constraint.cpp:37 +msgctxt "constr-name" +msgid "diameter" +msgstr "diamètre" + +#: constraint.cpp:38 +msgctxt "constr-name" +msgid "pt-on-circle" +msgstr "pt-sur-cercle" + +#: constraint.cpp:39 +msgctxt "constr-name" +msgid "same-orientation" +msgstr "même-orientation" + +#: constraint.cpp:40 +msgctxt "constr-name" +msgid "angle" +msgstr "angle" + +#: constraint.cpp:41 +msgctxt "constr-name" +msgid "parallel" +msgstr "parallèle" + +#: constraint.cpp:42 +msgctxt "constr-name" +msgid "arc-line-tangent" +msgstr "arc-ligne-tangente" + +#: constraint.cpp:43 +msgctxt "constr-name" +msgid "cubic-line-tangent" +msgstr "cubique-ligne-tangente" + +#: constraint.cpp:44 +msgctxt "constr-name" +msgid "curve-curve-tangent" +msgstr "courbe-courbe-tangente" + +#: constraint.cpp:45 +msgctxt "constr-name" +msgid "perpendicular" +msgstr "perpendiculaire" + +#: constraint.cpp:46 +msgctxt "constr-name" +msgid "eq-radius" +msgstr "eg-rayon" + +#: constraint.cpp:47 +msgctxt "constr-name" +msgid "eq-angle" +msgstr "eg-angle" + +#: constraint.cpp:48 +msgctxt "constr-name" +msgid "eq-line-len-arc-len" +msgstr "eg-ligne-long-arc-long" + +#: constraint.cpp:49 +msgctxt "constr-name" +msgid "lock-where-dragged" +msgstr "verrouillé-où-déplacé" + +#: constraint.cpp:50 +msgctxt "constr-name" +msgid "comment" +msgstr "commentaire" + +#: constraint.cpp:151 +msgid "" +"The point you selected does not belong to the arc. The arc and line segment " +"do not share an end point.\n" +"\n" +"Select the end point of the arc at which you want it to be tangent to the " +"line." +msgstr "" +"Le point sélectionné n'appartient pas à l'arc. L'arc et le segment de ligne " +"n'ont pas d'extrémité en commun.\n" +"\n" +"Sélectionnez une extrémité de l'arc que vous voulez être tangent à la ligne." + +#: constraint.cpp:158 +msgid "" +"The tangent arc and line segment must share an endpoint. Constrain them with " +"Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the arc at which you want it to be " +"tangent to the line." +msgstr "" +"L'arc tangent et le segment de ligne doivent partager une extrémité. " +"Contraignez-les avec « Contraintes -> Sur Point » avant de contraindre la " +"tangente.\n" +"\n" +"Alternativement, sélectionnez l'extrémité de l'arc que vous voulez " +"tangent à la ligne." + +#: constraint.cpp:186 +msgid "" +"The point you selected is not an end point of the cubic spline. The spline " +"and line segment do not share an end point.\n" +"\n" +"Select the end point of the spline at which you want it to be tangent to the " +"line." +msgstr "" +"Le point sélectionné n'est pas une extrémité de la spline cubique. La spline " +"et le segment de ligne n'ont pas d'extrémité en commun.\n" +"\n" +"Sélectionnez l'extrémité de la spline que vous voulez être tangente à la " +"ligne." + +#: constraint.cpp:193 +msgid "" +"The tangent cubic spline and line segment must share an endpoint. Constrain " +"them with Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the cubic spline at which you want it " +"to be tangent to the line." +msgstr "" +"La tangente cubique et le segment de ligne doivent partager une extrémité. " +"Contraignez-les avec « Contraintes -> Sur Point » avant de contraindre la " +"tangente.\n" +"\n" +"Alternativement, sélectionnez l'extrémité de la spline cubique où vous la " +"voulez tangent à la ligne." + +#: constraint.cpp:237 +msgid "" +"The points you selected are not end points of the two curves. The curves do " +"not share an end point.\n" +"\n" +"Select the end points of both curves at which you want them to be tangent to " +"each other." +msgstr "" +"Les points sélectionnés ne sont pas les extrémités des des courbes. Les " +"courbes n'ont pas d'extrémité en commun.\n" +"\n" +"Sélectionnez les extrémités des deux courbes que vous voulez tangentes." + +#: constraint.cpp:244 +msgid "" +"The curves must share an endpoint. Constrain them with Constrain -> On Point " +"before constraining tangent.\n" +"\n" +"Alternatively select the end points of both curves at which you want the " +"curves to be tangent." +msgstr "" +"Les courbes doivent partager une extrémité. Contraignez-les avec " +"« Contraintes -> Sur Point » avant de contraindre la tangente.\n" +"\n" +"Alternatively select the end points of both curves at which you want the " +"curves to be tangent." +"Alternativement, sélectionnez les extrémités des deux courbes là où vous " +"les voulez tangentes." + +#: constraint.cpp:303 +msgid "" +"Bad selection for distance / diameter constraint. This constraint can apply " +"to:\n" +"\n" +" * two points (distance between points)\n" +" * a line segment (length)\n" +" * two points and a line segment or normal (projected distance)\n" +" * a workplane and a point (minimum distance)\n" +" * a line segment and a point (minimum distance)\n" +" * a plane face and a point (minimum distance)\n" +" * a circle or an arc (diameter)\n" +msgstr "" +"Mauvaise sélection pour la contrainte distance / diamètre. Cette contrainte " +"peut s'appliquer à :\n" +"\n" +" – Deux points (distance entre points)\n" +" – Un segment de ligne (longueur)\n" +" – Deux points et un segment de ligne ou normal (distance projetée)\n" +" – Un plan de travail et un point (distance minimale)\n" +" – Un segment de ligne et un point (distance minimale)\n" +" – Une face plane et un point (distance minimale)\n" +" – Un cercle ou un arc (diamètre)\n" + +#: constraint.cpp:366 +msgid "" +"Bad selection for on point / curve / plane constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points (points coincident)\n" +" * a point and a workplane (point in plane)\n" +" * a point and a line segment (point on line)\n" +" * a point and a circle or arc (point on curve)\n" +" * a point and one to three plane faces (point on face(s))\n" +msgstr "" +"Mauvaise sélection pour la contrainte point / courbe / plan. Cette " +"contrainte peut s'appliquer à :\n" +"\n" +" – Au moins deux points (points coïncidents)\n" +" – Un point et un plan de travail (point dans le plan)\n" +" – Un point et un segment de ligne (point en ligne)\n" +" – Un point et un cercle ou un arc (point sur courbe)\n" +" – Un point, et entre une et trois faces planes (point sur face(s))\n" + +#: constraint.cpp:427 +msgid "" +"Bad selection for equal length / radius constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more line segments (equal length)\n" +" * two line segments and two points (equal point-line distances)\n" +" * a line segment and two points (equal point-line distances)\n" +" * a line segment, and a point and line segment (point-line distance " +"equals length)\n" +" * two or more circles or arcs (equal radius)\n" +" * a line segment and an arc (line segment length equals arc length)\n" +msgstr "" +"Mauvaise sélection pour une contrainte de longueur / rayon égale. Cette " +"contrainte peut s'appliquer à :\n" +"\n" +" – Au moins deux segments de ligne(s) (longueur égale)\n" +" – Deux segments de ligne et deux points (distances point-ligne égales)\n" +" – Un segment de ligne et deux points (distances point-ligne égales)\n" +" – Un segment de ligne ou un segment de ligne et point (distance point-" +"ligne de longueur égale)\n" +" – Au moins deux cercles ou arcs (rayon égaux)\n" +" – Un segment de ligne et un arc (la longueur de segment de ligne est " +"égale à la longueur d'arc)\n" + +#: constraint.cpp:480 +msgid "" +"Bad selection for length ratio constraint. This constraint can apply to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"Mauvaise sélection pour une contrainte de ratio de longueur. Cette " +"contrainte peut s'appliquer à :\n" +"\n" +" – Deux segments de ligne\n" +" – Deux arcs\n" +" – Un arc et un segment de ligne\n" + +#: constraint.cpp:515 +msgid "" +"Bad selection for length difference constraint. This constraint can apply " +"to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"Mauvaise sélection pour une contrainte de différence de longueur. Cette " +"contrainte peut s'appliquer à :\n" +"\n" +" – Deux segments de ligne\n" +" – Deux arcs\n" +" – Un arc et un segment de ligne\n" + +#: constraint.cpp:550 +msgid "" +"Bad selection for at midpoint constraint. This constraint can apply to:\n" +"\n" +" * a line segment and a point (point at midpoint)\n" +" * a line segment and a workplane (line's midpoint on plane)\n" +msgstr "" +"Mauvaise sélection pour une contrainte de point médian. Cette contrainte " +"peut s'appliquer à :\n" +"\n" +" – Un segment de ligne et un point (point au milieu)\n" +" – Un segment de ligne et un plan de travail (point médian dans le plan)\n" + +#: constraint.cpp:608 +msgid "" +"Bad selection for symmetric constraint. This constraint can apply to:\n" +"\n" +" * two points or a line segment (symmetric about workplane's coordinate " +"axis)\n" +" * line segment, and two points or a line segment (symmetric about line " +"segment)\n" +" * workplane, and two points or a line segment (symmetric about " +"workplane)\n" +msgstr "" +"Mauvaise sélection pour la contrainte symétrique. Cette contrainte peut " +"s'appliquer à :\n" +"\n" +" – Deux points ou un segment de ligne (symétrique à l'axe des coordonnées " +"du plan de travail)\n" +" – Segment de ligne, et deux points ou un segment de ligne (symétrique " +"sur le segment de ligne)\n" +" – Plan de travail, et deux points ou un segment de ligne (symétrique au " +"plan de travail)\n" + +#: constraint.cpp:623 +msgid "" +"A workplane must be active when constraining symmetric without an explicit " +"symmetry plane." +msgstr "" +"Un plan de travail doit être actif lors d'une contrainte de symétrie sans " +"plan de symétrie explicite." + +#: constraint.cpp:663 +msgid "" +"Activate a workplane (with Sketch -> In Workplane) before applying a " +"horizontal or vertical constraint." +msgstr "" +"Activez un plan de travail (avec Dessin -> Dans le plan de travail) avant " +"d'appliquer une contrainte horizontale ou verticale." + +#: constraint.cpp:679 +msgid "" +"Bad selection for horizontal / vertical constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points\n" +" * one or more line segments\n" +msgstr "" +"Mauvaise sélection pour la contrainte horizontale / verticale. Cette " +"contrainte peut s'appliquer à :\n" +"\n" +" – Au moins deux points\n" +" – Au moins un segment de ligne(s)\n" + +#: constraint.cpp:697 +msgid "" +"Bad selection for same orientation constraint. This constraint can apply " +"to:\n" +"\n" +" * two normals\n" +msgstr "" +"Mauvaise sélection pour la même contrainte d'orientation. Cette contrainte " +"peut s'appliquer à :\n" +"\n" +" – Deux normales\n" + +#: constraint.cpp:748 +msgid "Must select an angle constraint." +msgstr "Vous devez sélectionner une contrainte d'angle." + +#: constraint.cpp:761 +msgid "Must select a constraint with associated label." +msgstr "Vous devez sélectionner une contrainte avec une étiquette associée." + +#: constraint.cpp:784 +msgid "" +"Bad selection for angle constraint. This constraint can apply to:\n" +"\n" +"Angle between:\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +"\n" +"Equal angles:\n" +" * four line segments or normals (equal angle between A,B and C,D)\n" +" * three line segments or normals (equal angle between A,B and B,C)\n" +msgstr "" +"Mauvaise sélection pour une contrainte d'angle. Cette contrainte peut " +"s'appliquer à :\n" +"\n" +"Angle entre :\n" +" – Deux segments de ligne\n" +" – Un segment de ligne et une normale\n" +" – Deux normales\n" +"\n" +"Angles égaux :\n" +" – Quatre segments de ligne ou des normales (angle entre A, B et C, D " +"égaux)\n" +" – Trois segments de ligne ou des normales (angle entre A, B et B, C " +"égaux)\n" + +#: constraint.cpp:872 +msgid "Curve-curve tangency must apply in workplane." +msgstr "La tangence courbe-courbe doit s'appliquer dans le plan de travail." + +#: constraint.cpp:887 +msgid "" +"Bad selection for parallel / tangent constraint. This constraint can apply " +"to:\n" +"\n" +" * two faces\n" +" * two or more line segments (parallel)\n" +" * one or more line segments and one or more normals (parallel)\n" +" * two or more normals (parallel)\n" +" * two line segments, arcs, or beziers, that share an endpoint (tangent)\n" +" * two line segments, arcs, or beziers, that do not share an endpoint and " +"the end point(s) of the curve(s) (tangent)\n" +msgstr "" +"Mauvaise sélection pour la contrainte parallèle / tangente. Cette contrainte " +"peut s'appliquer à :\n" +"\n" +" – Deux faces\n" +" – Au moins deux segments de ligne(s) (parallèle)\n" +" – Au moins un segment de ligne et au moins une normale (parallèle)\n" +" – Au moins deux normales (parallèle)\n" +" – Deux segments de ligne, des arcs ou des Béziers, qui partagent une " +"extrémité (tangente)\n" +" * Deux segments de lignes, arcs, ou béziers, qui n'ont pas d'extrémité " +"en commun et la/les extrémité(s) des courbe(s) (tangente)\n" + +#: constraint.cpp:914 +msgid "" +"Bad selection for perpendicular constraint. This constraint can apply to:\n" +"\n" +" * two faces\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +msgstr "" +"Mauvaise sélection pour une contrainte perpendiculaire. Cette contrainte " +"peut s'appliquer à :\n" +"\n" +" – Deux faces\n" +" – Deux segments de ligne\n" +" – Un segment de ligne et une normale\n" +" – Deux normales\n" + +#: constraint.cpp:931 +msgid "" +"Bad selection for lock point where dragged constraint. This constraint can " +"apply to:\n" +"\n" +" * a point\n" +msgstr "" +"Mauvaise sélection pour le point de verrouillage où la contrainte déplacé. " +"Cette contrainte peut s'appliquer à :\n" +"\n" +" – un point\n" + +#: constraint.cpp:946 mouse.cpp:1160 +msgid "NEW COMMENT -- DOUBLE-CLICK TO EDIT" +msgstr "NOUVEAU COMMENTAIRE — DOUBLE-CLIQUEZ POUR EDITER" + +#: constraint.cpp:952 +msgid "click center of comment text" +msgstr "cliquez le centre du texte de commentaire" + +#: export.cpp:19 +msgid "" +"No solid model present; draw one with extrudes and revolves, or use Export " +"2d View to export bare lines and curves." +msgstr "" +"Aucun modèle solide présent ; dessinez-en un avec une extrusion et " +"révolution, ou utilisez « Exporter vue 2d » pour exporter les lignes et les " +"courbes dépouillées." + +#: export.cpp:61 +msgid "" +"Bad selection for export section. Please select:\n" +"\n" +" * nothing, with an active workplane (workplane is section plane)\n" +" * a face (section plane through face)\n" +" * a point and two line segments (plane through point and parallel to " +"lines)\n" +msgstr "" +"Mauvaise sélection pour la section export. Sélectionnez :\n" +"\n" +" – Rien, avec un plan de travail actif (plan de travail est un plan de " +"section)\n" +" – Une face (plan de coupe au-travers d'une face)\n" +" – Un point et deux segments de ligne (plan au-travers d'un point et " +"parallèle aux lignes)\n" + +#: export.cpp:818 +msgid "Active group mesh is empty; nothing to export." +msgstr "Le maillage du groupe actif est vide ; Rien à exporter." + +#: exportvector.cpp:336 +msgid "freehand lines were replaced with continuous lines" +msgstr "les lignes à main levée ont été remplacées par des lignes continues" + +#: exportvector.cpp:338 +msgid "zigzag lines were replaced with continuous lines" +msgstr "les lignes en zigzag ont été remplacées par des lignes continues" + +#: exportvector.cpp:592 +msgid "" +"Some aspects of the drawing have no DXF equivalent and were not exported:\n" +msgstr "" +"Certains aspects du dessin n'ont pas d'équivalent DXF et n'ont pas été " +"exportés :\n" + +#: exportvector.cpp:838 +msgid "" +"PDF page size exceeds 200 by 200 inches; many viewers may reject this file." +msgstr "" +"La taille de la page PDF dépasse 200 par 200 pouces ; de nombreux lecteurs " +"risquent rejeter ce fichier." + +#: file.cpp:44 group.cpp:91 +msgctxt "group-name" +msgid "sketch-in-plane" +msgstr "dessin-dans-plan" + +#: file.cpp:62 +msgctxt "group-name" +msgid "#references" +msgstr "#références" + +#: file.cpp:555 +msgid "The file is empty. It may be corrupt." +msgstr "Le fichier est vide. Il est possible qu'il soit corrompu." + +#: file.cpp:560 +msgid "" +"Unrecognized data in file. This file may be corrupt, or from a newer version " +"of the program." +msgstr "" +"Données non reconnues dans le fichier. Ce fichier peut être corrompu, ou " +"venir d'une version plus récente du programme." + +#: file.cpp:876 +msgctxt "title" +msgid "Missing File" +msgstr "Fichier manquant" + +#: file.cpp:877 +#, c-format +msgctxt "dialog" +msgid "The linked file “%s” is not present." +msgstr "Le fichier lié « %s » n'est pas présent." + +#: file.cpp:879 +msgctxt "dialog" +msgid "" +"Do you want to locate it manually?\n" +"\n" +"If you decline, any geometry that depends on the missing file will be " +"permanently removed." +msgstr "" +"Voulez-vous le chercher manuellement ?\n" +"\n" +"Sinon, toute géométrie qui dépend du fichier manquant sera définitivement " +"supprimée." + +#: file.cpp:882 +msgctxt "button" +msgid "&Yes" +msgstr "&Oui" + +#: file.cpp:884 +msgctxt "button" +msgid "&No" +msgstr "&Non" + +#: file.cpp:886 solvespace.cpp:652 +msgctxt "button" +msgid "&Cancel" +msgstr "&Annuler" + +#: graphicswin.cpp:41 +msgid "&File" +msgstr "&Fichier" + +#: graphicswin.cpp:42 +msgid "&New" +msgstr "&Nouveau" + +#: graphicswin.cpp:43 +msgid "&Open..." +msgstr "&Ouvrir..." + +#: graphicswin.cpp:44 +msgid "Open &Recent" +msgstr "Ouvrir &Récent" + +#: graphicswin.cpp:45 +msgid "&Save" +msgstr "&Sauver" + +#: graphicswin.cpp:46 +msgid "Save &As..." +msgstr "Sauver &Comme..." + +#: graphicswin.cpp:48 +msgid "Export &Image..." +msgstr "Exporter &Image..." + +#: graphicswin.cpp:49 +msgid "Export 2d &View..." +msgstr "Exporter &vue 2D..." + +#: graphicswin.cpp:50 +msgid "Export 2d &Section..." +msgstr "Exporter &Section 2d..." + +#: graphicswin.cpp:51 +msgid "Export 3d &Wireframe..." +msgstr "Exporter &Fil de fer 3d..." + +#: graphicswin.cpp:52 +msgid "Export Triangle &Mesh..." +msgstr "Exporter Triangle &Maillage..." + +#: graphicswin.cpp:53 +msgid "Export &Surfaces..." +msgstr "Export &Surfaces..." + +#: graphicswin.cpp:54 +msgid "Im&port..." +msgstr "Im&porter..." + +#: graphicswin.cpp:57 +msgid "E&xit" +msgstr "&Quitter" + +#: graphicswin.cpp:60 +msgid "&Edit" +msgstr "&Editer" + +#: graphicswin.cpp:61 +msgid "&Undo" +msgstr "&Annuler" + +#: graphicswin.cpp:62 +msgid "&Redo" +msgstr "&Refaire" + +#: graphicswin.cpp:63 +msgid "Re&generate All" +msgstr "Re&générer Tout" + +#: graphicswin.cpp:65 +msgid "Snap Selection to &Grid" +msgstr "Accrocher la sélection à la &Grille" + +#: graphicswin.cpp:66 +msgid "Rotate Imported &90°" +msgstr "Tourner l'import de &90°" + +#: graphicswin.cpp:68 +msgid "Cu&t" +msgstr "Co&uper" + +#: graphicswin.cpp:69 +msgid "&Copy" +msgstr "&Copier" + +#: graphicswin.cpp:70 +msgid "&Paste" +msgstr "Co&ller" + +#: graphicswin.cpp:71 +msgid "Paste &Transformed..." +msgstr "Coller &Transformer..." + +#: graphicswin.cpp:72 +msgid "&Delete" +msgstr "&Effacer" + +#: graphicswin.cpp:74 +msgid "Select &Edge Chain" +msgstr "Sélectionner une Chaîne d'&Arêtes" + +#: graphicswin.cpp:75 +msgid "Select &All" +msgstr "Sélectionner &Tout" + +#: graphicswin.cpp:76 +msgid "&Unselect All" +msgstr "&Désélectionner Tout" + +#: graphicswin.cpp:78 +msgid "&Line Styles..." +msgstr "&Styles de Ligne..." + +#: graphicswin.cpp:79 +msgid "&View Projection..." +msgstr "&Affichage Perspective..." + +#: graphicswin.cpp:81 +msgid "Con&figuration..." +msgstr "Con&figuration..." + +#: graphicswin.cpp:84 +msgid "&View" +msgstr "&Affichage" + +#: graphicswin.cpp:85 +msgid "Zoom &In" +msgstr "Zoom &Avant" + +#: graphicswin.cpp:86 +msgid "Zoom &Out" +msgstr "Zoom A&rrière" + +#: graphicswin.cpp:87 +msgid "Zoom To &Fit" +msgstr "Zoom A&justé" + +#: graphicswin.cpp:89 +msgid "Align View to &Workplane" +msgstr "Aligner la vue au &Plan de travail" + +#: graphicswin.cpp:90 +msgid "Nearest &Ortho View" +msgstr "Vue &Orthogonale la plus proche" + +#: graphicswin.cpp:91 +msgid "Nearest &Isometric View" +msgstr "Vue &Isométrique la plus proche" + +#: graphicswin.cpp:92 +msgid "&Center View At Point" +msgstr "&Centrer la Vue sur le Point" + +#: graphicswin.cpp:94 +msgid "Show Snap &Grid" +msgstr "Afficher la &grille d'accrochage" + +#: graphicswin.cpp:95 +msgid "Darken Inactive Solids" +msgstr "Noircir Solides Inactifs" + +#: graphicswin.cpp:96 +msgid "Use &Perspective Projection" +msgstr "Utiliser la Vue en &Perspective" + +#: graphicswin.cpp:97 +msgid "Show E&xploded View" +msgstr "Afficher Vue Éclatée" + +#: graphicswin.cpp:98 +msgid "Dimension &Units" +msgstr "&Unités de dimensions" + +#: graphicswin.cpp:99 +msgid "Dimensions in &Millimeters" +msgstr "Dimensions en &Millimètres" + +#: graphicswin.cpp:100 +msgid "Dimensions in M&eters" +msgstr "Dimensions en &Mètres" + +#: graphicswin.cpp:101 +msgid "Dimensions in &Inches" +msgstr "Dimensions en &Pouces" + +#: graphicswin.cpp:102 +msgid "Dimensions in &Feet and Inches" +msgstr "Dimensions en &Pieds et Pouces" + +#: graphicswin.cpp:104 +msgid "Show &Toolbar" +msgstr "Affichage &Barre d'outils" + +#: graphicswin.cpp:105 +msgid "Show Property Bro&wser" +msgstr "Affichage du &Navigateur de Propriété" + +#: graphicswin.cpp:107 +msgid "&Full Screen" +msgstr "&Plein Ecran" + +#: graphicswin.cpp:109 +msgid "&New Group" +msgstr "&Nouveau Groupe" + +#: graphicswin.cpp:110 +msgid "Sketch In &3d" +msgstr "Dessin en &3d" + +#: graphicswin.cpp:111 +msgid "Sketch In New &Workplane" +msgstr "Dessin dans un nouveau &Plan de travail" + +#: graphicswin.cpp:113 +msgid "Step &Translating" +msgstr "Répéter par &Translation" + +#: graphicswin.cpp:114 +msgid "Step &Rotating" +msgstr "Répéter par &Rotation" + +#: graphicswin.cpp:116 +msgid "E&xtrude" +msgstr "E&xtruder" + +#: graphicswin.cpp:117 +msgid "&Helix" +msgstr "&Hélice" + +#: graphicswin.cpp:118 +msgid "&Lathe" +msgstr "&Tour (révolution complète)" + +#: graphicswin.cpp:119 +msgid "Re&volve" +msgstr "Ré&volution" + +#: graphicswin.cpp:121 +msgid "Link / Assemble..." +msgstr "Lier / Assembler..." + +#: graphicswin.cpp:122 +msgid "Link Recent" +msgstr "Lier Récent" + +#: graphicswin.cpp:124 +msgid "&Sketch" +msgstr "&Dessin" + +#: graphicswin.cpp:125 +msgid "In &Workplane" +msgstr "Dans le &Plan de travail" + +#: graphicswin.cpp:126 +msgid "Anywhere In &3d" +msgstr "N'importe où dans la &3d" + +#: graphicswin.cpp:128 +msgid "Datum &Point" +msgstr "&Point" + +#: graphicswin.cpp:129 +msgid "Wor&kplane" +msgstr "&Plan de travail" + +#: graphicswin.cpp:131 +msgid "Line &Segment" +msgstr "Ligne - &Polyligne" + +#: graphicswin.cpp:132 +msgid "C&onstruction Line Segment" +msgstr "Ligne de C&onstruction" + +#: graphicswin.cpp:133 +msgid "&Rectangle" +msgstr "&Rectangle" + +#: graphicswin.cpp:134 +msgid "&Circle" +msgstr "&Cercle" + +#: graphicswin.cpp:135 +msgid "&Arc of a Circle" +msgstr "&Arc de Cercle" + +#: graphicswin.cpp:136 +msgid "&Bezier Cubic Spline" +msgstr "Spline Cubique de &Bézier" + +#: graphicswin.cpp:138 +msgid "&Text in TrueType Font" +msgstr "&Texte en Police TrueType" + +#: graphicswin.cpp:139 +msgid "I&mage" +msgstr "I&mage" + +#: graphicswin.cpp:141 +msgid "To&ggle Construction" +msgstr "&Basculer en mode « Construction »" + +#: graphicswin.cpp:142 +msgid "Ta&ngent Arc at Point" +msgstr "Rendre l'Arc Ta&ngent au Point" + +#: graphicswin.cpp:143 +msgid "Split Curves at &Intersection" +msgstr "Diviser les Courbes à l'&Intersection" + +#: graphicswin.cpp:145 +msgid "&Constrain" +msgstr "&Contraintes" + +#: graphicswin.cpp:146 +msgid "&Distance / Diameter" +msgstr "&Distance / Diamètre" + +#: graphicswin.cpp:147 +msgid "Re&ference Dimension" +msgstr "Dimension Maîtresse / Indicative" + +#: graphicswin.cpp:148 +msgid "A&ngle / Equal Angle" +msgstr "A&ngle / Égalité Angle" + +#: graphicswin.cpp:149 +msgid "Reference An&gle" +msgstr "An&gle Maître / Indicatif" + +#: graphicswin.cpp:150 +msgid "Other S&upplementary Angle" +msgstr "Autre angle S&upplémentaire" + +#: graphicswin.cpp:151 +msgid "Toggle R&eference Dim" +msgstr "Basculer cote Maîtresse / cote Indicative" + +#: graphicswin.cpp:153 +msgid "&Horizontal" +msgstr "&Horizontal" + +#: graphicswin.cpp:154 +msgid "&Vertical" +msgstr "&Vertical" + +#: graphicswin.cpp:156 +msgid "&On Point / Curve / Plane" +msgstr "&Sur Point / Courbe / Plan" + +#: graphicswin.cpp:157 +msgid "E&qual Length / Radius" +msgstr "É&galité Longueur / Rayon" + +#: graphicswin.cpp:158 +msgid "Length / Arc Ra&tio" +msgstr "Ratio Longueur / Arc" + +#: graphicswin.cpp:159 +msgid "Length / Arc Diff&erence" +msgstr "Différence Longueur / Arc" + +#: graphicswin.cpp:160 +msgid "At &Midpoint" +msgstr "Au &Milieu" + +#: graphicswin.cpp:161 +msgid "S&ymmetric" +msgstr "&Symétrique" + +#: graphicswin.cpp:162 +msgid "Para&llel / Tangent" +msgstr "Para&llèle / Tangent" + +#: graphicswin.cpp:163 +msgid "&Perpendicular" +msgstr "&Perpendiculaire" + +#: graphicswin.cpp:164 +msgid "Same Orient&ation" +msgstr "Même Orient&ation" + +#: graphicswin.cpp:165 +msgid "Lock Point Where &Dragged" +msgstr "Accrocher le point à l'&Emplacement" + +#: graphicswin.cpp:167 +msgid "Comment" +msgstr "Commentaire" + +#: graphicswin.cpp:169 +msgid "&Analyze" +msgstr "&Analyse" + +#: graphicswin.cpp:170 +msgid "Measure &Volume" +msgstr "Mesurer &Volume" + +#: graphicswin.cpp:171 +msgid "Measure A&rea" +msgstr "Mesurer &Aire" + +#: graphicswin.cpp:172 +msgid "Measure &Perimeter" +msgstr "Mesurer &Périmètre" + +#: graphicswin.cpp:173 +msgid "Show &Interfering Parts" +msgstr "Montrer les Pièces &Interférentes" + +#: graphicswin.cpp:174 +msgid "Show &Naked Edges" +msgstr "Montrer les Arêtes &Nues" + +#: graphicswin.cpp:175 +msgid "Show &Center of Mass" +msgstr "Montrer le &Centre de Gravité" + +#: graphicswin.cpp:177 +msgid "Show &Underconstrained Points" +msgstr "Montrer les &sous-contraintes Points" + +#: graphicswin.cpp:179 +msgid "&Trace Point" +msgstr "&Tracer Point" + +#: graphicswin.cpp:180 +msgid "&Stop Tracing..." +msgstr "&Arrêter Tracé..." + +#: graphicswin.cpp:181 +msgid "Step &Dimension..." +msgstr "&Dimension pas-à-pas..." + +#: graphicswin.cpp:183 +msgid "&Help" +msgstr "&Aide" + +#: graphicswin.cpp:184 +msgid "&Language" +msgstr "&Langue" + +#: graphicswin.cpp:185 +msgid "&Website / Manual" +msgstr "&Site web / Manuel" + +#: graphicswin.cpp:186 +msgid "&Go to GitHub commit" +msgstr "Voir le commit sur &GitHub" + +#: graphicswin.cpp:188 +msgid "&About" +msgstr "&A propos" + +#: graphicswin.cpp:362 +msgid "(no recent files)" +msgstr "(pas de fichier récent)" + +#: graphicswin.cpp:370 +#, c-format +msgid "File '%s' does not exist." +msgstr "Le fichier « %s » n'existe pas." + +#: graphicswin.cpp:779 +msgid "No workplane is active, so the grid will not appear." +msgstr "Pas de plan de travail actif, la grille ne va donc pas apparaître." + +#: graphicswin.cpp:794 +msgid "" +"The perspective factor is set to zero, so the view will always be a parallel " +"projection.\n" +"\n" +"For a perspective projection, modify the perspective factor in the " +"configuration screen. A value around 0.3 is typical." +msgstr "" +"Le facteur de perspective est réglé à 0, donc la vue restera une projection " +"parallèle.\n" +"\n" +"Pour une projection en perspective, modifiez le facteur de perspective dans " +"l'écran de configuration. Une valeur d'environ 0,3 est typique." + +#: graphicswin.cpp:884 +msgid "" +"Select a point; this point will become the center of the view on screen." +msgstr "" +"Sélectionnez un point. Ce point deviendra le centre de la vue à l'écran." + +#: graphicswin.cpp:1193 +msgid "No additional entities share endpoints with the selected entities." +msgstr "" +"Aucune entité supplémentaire ne partage des points d'extrémité avec les " +"entités sélectionnées." + +#: graphicswin.cpp:1211 +msgid "" +"To use this command, select a point or other entity from an linked part, or " +"make a link group the active group." +msgstr "" +"Pour utiliser cette commande, sélectionnez un point ou une autre entité à " +"partir d'une pièce liée ou créez un groupe de liens dans le groupe actif." + +#: graphicswin.cpp:1234 +msgid "" +"No workplane is active. Activate a workplane (with Sketch -> In Workplane) " +"to define the plane for the snap grid." +msgstr "" +"Aucun plan de travail n'est actif. Activez un plan de travail (avec Dessin -" +"> Dans le plan de travail) pour définir le plan pour la grille d'accrochage." + +#: graphicswin.cpp:1241 +msgid "" +"Can't snap these items to grid; select points, text comments, or constraints " +"with a label. To snap a line, select its endpoints." +msgstr "" +"Impossible d'accrocher ces éléments à la grille. Sélectionnez des points, " +"des textes de commentaires ou des contraintes avec une étiquette. Pour " +"accrocher une ligne, sélectionnez ses extrémités." + +#: graphicswin.cpp:1326 +msgid "No workplane selected. Activating default workplane for this group." +msgstr "" +"Aucun plan de travail sélectionné. Activation du plan de travail par défaut " +"pour ce groupe." + +#: graphicswin.cpp:1329 +msgid "" +"No workplane is selected, and the active group does not have a default " +"workplane. Try selecting a workplane, or activating a sketch-in-new-" +"workplane group." +msgstr "" +"Aucun plan de travail n'est sélectionné et le groupe actif n'a pas de plan " +"de travail par défaut. Essayez de sélectionner un plan de travail ou " +"d'activer un groupe de « Dessin dans nouveau plan travail »." + +#: graphicswin.cpp:1350 +msgid "" +"Bad selection for tangent arc at point. Select a single point, or select " +"nothing to set up arc parameters." +msgstr "" +"Mauvaise sélection pour l'arc tangent au point. Sélectionnez un seul point, " +"ou ne sélectionnez rien pour configurer les paramètres de l'arc." + +#: graphicswin.cpp:1361 +msgid "click point on arc (draws anti-clockwise)" +msgstr "cliquez un point sur l'arc (dessine dans le sens anti-horaire)" + +#: graphicswin.cpp:1362 +msgid "click to place datum point" +msgstr "cliquez pour placer un point" + +#: graphicswin.cpp:1363 +msgid "click first point of line segment" +msgstr "cliquez le premier point du segment de ligne" + +#: graphicswin.cpp:1365 +msgid "click first point of construction line segment" +msgstr "cliquez le premier point de la ligne de construction" + +#: graphicswin.cpp:1366 +msgid "click first point of cubic segment" +msgstr "cliquez le premier point du segment cubique" + +#: graphicswin.cpp:1367 +msgid "click center of circle" +msgstr "cliquez pour placer le centre du cercle" + +#: graphicswin.cpp:1368 +msgid "click origin of workplane" +msgstr "cliquez pour placer l'origine du plan de travail" + +#: graphicswin.cpp:1369 +msgid "click one corner of rectangle" +msgstr "cliquez un coin du rectangle" + +#: graphicswin.cpp:1370 +msgid "click top left of text" +msgstr "cliquez le haut à gauche du texte" + +#: graphicswin.cpp:1376 +msgid "click top left of image" +msgstr "cliquez le haut à gauche de l'image" + +#: graphicswin.cpp:1402 +msgid "" +"No entities are selected. Select entities before trying to toggle their " +"construction state." +msgstr "" +"Aucune entité n'est sélectionnée. Sélectionnez les entités avant d'essayer " +"de basculer leurs états de construction." + +#: group.cpp:86 +msgctxt "group-name" +msgid "sketch-in-3d" +msgstr "dessin-en-3d" + +#: group.cpp:154 +msgid "" +"Bad selection for new sketch in workplane. This group can be created with:\n" +"\n" +" * a point (through the point, orthogonal to coordinate axes)\n" +" * a point and two line segments (through the point, parallel to the " +"lines)\n" +" * a point and a normal (through the point, orthogonal to the normal)\n" +" * a workplane (copy of the workplane)\n" +msgstr "" +"Mauvaise sélection pour un nouveau dessin. Ce groupe peut être créé avec :\n" +"\n" +" – Un point (passant par le point, orthogonal aux axes de coordonnées)\n" +" – Un point et deux segments de lignes (passant par le point, parallèle " +"aux deux lignes)\n" +" – Un point et une normale (passant par le point, orthogonal à la " +"normale)\n" +" – Un plan de travail (copie du plan de travail)\n" + +#: group.cpp:170 +msgid "" +"Activate a workplane (Sketch -> In Workplane) before extruding. The sketch " +"will be extruded normal to the workplane." +msgstr "" +"Activez un plan de travail (Dessin -> Dans le plan de travail) avant " +"l'extrusion. Le croquis sera extrudé normalement au plan de travail." + +#: group.cpp:179 +msgctxt "group-name" +msgid "extrude" +msgstr "extruder" + +#: group.cpp:184 +msgid "Lathe operation can only be applied to planar sketches." +msgstr "L'opération tour ne peut être appliquée qu'à des dessins plans." + +#: group.cpp:195 +msgid "" +"Bad selection for new lathe group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Mauvaise sélection pour un nouveau groupe de révolution. Ce groupe peut être " +"créé avec :\n" +"\n" +" – Un point et un segment de ligne ou normal (révolution autour d'un axe " +"parallèle à la ligne / point normal, par le point)\n" +" – Un segment de ligne (révolution sur le segment de ligne)\n" + +#: group.cpp:205 +msgctxt "group-name" +msgid "lathe" +msgstr "tour" + +#: group.cpp:210 +msgid "Revolve operation can only be applied to planar sketches." +msgstr "L'opération révolution ne peut être appliquée qu'à des dessins plans." + +#: group.cpp:221 +msgid "" +"Bad selection for new revolve group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Mauvaise sélection pour un nouveau groupe de révolution. Ce groupe peut être " +"créé avec :\n" +"\n" +" – Un point et un segment de ligne ou normal (révolution autour d'un axe " +"parallèle à la ligne / point normal, par le point)\n" +" – Un segment de ligne (révolution sur le segment de ligne)\n" + +#: group.cpp:233 +msgctxt "group-name" +msgid "revolve" +msgstr "révolution" + +#: group.cpp:238 +msgid "Helix operation can only be applied to planar sketches." +msgstr "L'opération hélice ne peut être appliquée qu'à des dessins plans." + +#: group.cpp:249 +msgid "" +"Bad selection for new helix group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Mauvaise sélection pour un nouveau groupe d'hélice. Ce groupe peut être créé " +"avec :\n" +"\n" +" – Un point et un segment de ligne ou normal (révolution autour d'un axe " +"parallèle à la ligne / point normal, par le point)\n" +" – Un segment de ligne (révolution sur le segment de ligne)\n" + +#: group.cpp:261 +msgctxt "group-name" +msgid "helix" +msgstr "hélice" + +#: group.cpp:274 +msgid "" +"Bad selection for new rotation. This group can be created with:\n" +"\n" +" * a point, while locked in workplane (rotate in plane, about that " +"point)\n" +" * a point and a line or a normal (rotate about an axis through the " +"point, and parallel to line / normal)\n" +msgstr "" +"Mauvaise sélection pour une nouvelle rotation. Ce groupe peut être créé " +"avec :\n" +"\n" +" – Un point, lorsqu'il est verrouillé dans un plan de travail (rotation " +"dans le plan, autour de ce point)\n" +" – Un point et une ligne ou une normale (tourner autour d'un axe par le " +"point et parallèle à la ligne / normale)\n" + +#: group.cpp:287 +msgctxt "group-name" +msgid "rotate" +msgstr "rotation" + +#: group.cpp:298 +msgctxt "group-name" +msgid "translate" +msgstr "translation" + +#: group.cpp:422 +msgid "(unnamed)" +msgstr "(sans nom)" + +#: groupmesh.cpp:710 +msgid "not closed contour, or not all same style!" +msgstr "contour non fermé ou tout n'est pas du même style !" + +#: groupmesh.cpp:723 +msgid "points not all coplanar!" +msgstr "les points ne sont pas tous coplanaires !" + +#: groupmesh.cpp:725 +msgid "contour is self-intersecting!" +msgstr "le contour s'entrecroise !" + +#: groupmesh.cpp:727 +msgid "zero-length edge!" +msgstr "arête de longueur nulle !" + +#: importmesh.cpp:136 +msgid "Text-formated STL files are not currently supported" +msgstr "Les fichiers STL textuels ne sont pas actuellement pas supportés" + +#: modify.cpp:252 +msgid "Must be sketching in workplane to create tangent arc." +msgstr "Vous devez dessiner dans un plan pour créer un arc tangent." + +#: modify.cpp:299 +msgid "" +"To create a tangent arc, select a point where two non-construction lines or " +"circles in this group and workplane join." +msgstr "" +"Pour créer un arc tangent, sélectionnez un point où deux lignes ou cercles " +"(pas de construction) de ce groupe et de ce plan se rejoignent." + +#: modify.cpp:386 +msgid "" +"Couldn't round this corner. Try a smaller radius, or try creating the " +"desired geometry by hand with tangency constraints." +msgstr "" +"Impossible d'arrondir ce coin. Essayez un rayon plus petit, ou essayez de " +"créer la géométrie souhaitée à la main avec des contraintes tangentielles." + +#: modify.cpp:595 +msgid "Couldn't split this entity; lines, circles, or cubics only." +msgstr "" +"Impossible de diviser cette entité ; Lignes, cercles ou cubiques uniquement." + +#: modify.cpp:622 +msgid "Must be sketching in workplane to split." +msgstr "Vous devez dessiner dans un plan de travail pour diviser." + +#: modify.cpp:629 +msgid "" +"Select two entities that intersect each other (e.g. two lines/circles/arcs " +"or a line/circle/arc and a point)." +msgstr "" +"Sélectionnez deux entités qui s'intersectent (par exemple deux lignes/" +"cercles/arcs, ou une ligne/cercle/arc et un point)." + +#: modify.cpp:734 +msgid "Can't split; no intersection found." +msgstr "Impossible de diviser ; pas d'intersection trouvée." + +#: mouse.cpp:558 +msgid "Assign to Style" +msgstr "Appliquer au style" + +#: mouse.cpp:574 +msgid "No Style" +msgstr "Pas de style" + +#: mouse.cpp:577 +msgid "Newly Created Custom Style..." +msgstr "Style personnalisé nouvellement créé ..." + +#: mouse.cpp:584 +msgid "Group Info" +msgstr "Info Groupe" + +#: mouse.cpp:604 +msgid "Style Info" +msgstr "Info Style" + +#: mouse.cpp:624 +msgid "Select Edge Chain" +msgstr "Sélection Chaîne d'arêtes" + +#: mouse.cpp:630 +msgid "Toggle Reference Dimension" +msgstr "Basculer cote maîtresse / cote indicative" + +#: mouse.cpp:636 +msgid "Other Supplementary Angle" +msgstr "Autre angle supplémentaire" + +#: mouse.cpp:641 +msgid "Snap to Grid" +msgstr "Accrocher à la grille" + +#: mouse.cpp:650 +msgid "Remove Spline Point" +msgstr "Effacer le point de la Spline" + +#: mouse.cpp:685 +msgid "Add Spline Point" +msgstr "Ajouter un point à la Spline" + +#: mouse.cpp:689 +msgid "Cannot add spline point: maximum number of points reached." +msgstr "" +"Impossible d'ajouter le point spline : nombre maximum de points atteints." + +#: mouse.cpp:714 +msgid "Toggle Construction" +msgstr "Basculer en mode « construction »" + +#: mouse.cpp:730 +msgid "Delete Point-Coincident Constraint" +msgstr "Effacer la contraint Point-Coïncident" + +#: mouse.cpp:748 +msgid "Cut" +msgstr "Couper" + +#: mouse.cpp:750 +msgid "Copy" +msgstr "Copier" + +#: mouse.cpp:754 +msgid "Select All" +msgstr "Sélectionner tout" + +#: mouse.cpp:759 +msgid "Paste" +msgstr "Coller" + +#: mouse.cpp:761 +msgid "Paste Transformed..." +msgstr "Coller transformé..." + +#: mouse.cpp:766 +msgid "Delete" +msgstr "Effacer" + +#: mouse.cpp:769 +msgid "Unselect All" +msgstr "Désélectionner tout" + +#: mouse.cpp:776 +msgid "Unselect Hovered" +msgstr "Désélectionner survolé" + +#: mouse.cpp:785 +msgid "Zoom to Fit" +msgstr "Zoom pour ajuster" + +#: mouse.cpp:987 mouse.cpp:1276 +msgid "click next point of line, or press Esc" +msgstr "cliquez pou le prochain point de ligne or appuyez sur Esc" + +#: mouse.cpp:993 +msgid "" +"Can't draw rectangle in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Impossible de dessiner un rectangle en 3d ; D'abord, activez un plan de " +"travail avec « Dessin -> Dans le plan de travail »." + +#: mouse.cpp:1027 +msgid "click to place other corner of rectangle" +msgstr "cliquez pour placer un autre coin de rectangle" + +#: mouse.cpp:1048 +msgid "click to set radius" +msgstr "cliquez pour ajuster le rayon" + +#: mouse.cpp:1053 +msgid "" +"Can't draw arc in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Ne peut pas dessiner l'arc en 3d ; D'abord, activez un plan de travail avec " +"« Dessin -> Dans le plan de travail »." + +#: mouse.cpp:1072 +msgid "click to place point" +msgstr "cliquez pour placer un point" + +#: mouse.cpp:1088 +msgid "click next point of cubic, or press Esc" +msgstr "cliquez le prochain point cubique ou appuyez sur Esc" + +#: mouse.cpp:1093 +msgid "" +"Sketching in a workplane already; sketch in 3d before creating new workplane." +msgstr "" +"Vous dessinez déjà dans un plan de travail ; Sélectionner « Dessiner en 3d » " +"avant de créer un nouveau plan de travail." + +#: mouse.cpp:1109 +msgid "" +"Can't draw text in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Impossible de dessiner du texte en 3d ; D'abord, activer un plan de travail " +"avec « Dessin -> Dans le plan de travail »." + +#: mouse.cpp:1126 +msgid "click to place bottom right of text" +msgstr "cliquer pour placer le coin inférieur droit du texte" + +#: mouse.cpp:1132 +msgid "" +"Can't draw image in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Impossible de dessiner l'image en 3d ; D'abord, activez un plan de travail " +"avec « Dessin -> Dans le plan de travail »." + +#: platform/gui.cpp:85 platform/gui.cpp:90 solvespace.cpp:583 +msgctxt "file-type" +msgid "SolveSpace models" +msgstr "modèles SolveSpace" + +#: platform/gui.cpp:89 +msgctxt "file-type" +msgid "ALL" +msgstr "TOUT" + +#: platform/gui.cpp:91 +msgctxt "file-type" +msgid "IDF circuit board" +msgstr "circuit imprimé IDF" + +#: platform/gui.cpp:92 +msgctxt "file-type" +msgid "STL triangle mesh" +msgstr "maillage de triangles STL" + +#: platform/gui.cpp:96 +msgctxt "file-type" +msgid "PNG image" +msgstr "image PNG" + +#: platform/gui.cpp:100 +msgctxt "file-type" +msgid "STL mesh" +msgstr "maillage STL" + +#: platform/gui.cpp:101 +msgctxt "file-type" +msgid "Wavefront OBJ mesh" +msgstr "Mesh Wavefront OBJ" + +#: platform/gui.cpp:102 +msgctxt "file-type" +msgid "Three.js-compatible mesh, with viewer" +msgstr "Mesh compatible avec Three.js, avec visionneuse" + +#: platform/gui.cpp:103 +msgctxt "file-type" +msgid "Three.js-compatible mesh, mesh only" +msgstr "Mesh compatible avec Three.js, mesh seulement" + +#: platform/gui.cpp:104 +msgctxt "file-type" +msgid "VRML text file" +msgstr "Fichier texte VRML" + +#: platform/gui.cpp:108 platform/gui.cpp:115 platform/gui.cpp:122 +msgctxt "file-type" +msgid "STEP file" +msgstr "fichier STEP" + +#: platform/gui.cpp:112 +msgctxt "file-type" +msgid "PDF file" +msgstr "fichier PDF" + +#: platform/gui.cpp:113 +msgctxt "file-type" +msgid "Encapsulated PostScript" +msgstr "PostScript encapsulé" + +#: platform/gui.cpp:114 +msgctxt "file-type" +msgid "Scalable Vector Graphics" +msgstr "image vectorielle SVG" + +#: platform/gui.cpp:116 platform/gui.cpp:123 +msgctxt "file-type" +msgid "DXF file (AutoCAD 2007)" +msgstr "fichier DXF (AutoCAD 2007)" + +#: platform/gui.cpp:117 +msgctxt "file-type" +msgid "HPGL file" +msgstr "fichier HPGL" + +#: platform/gui.cpp:118 +msgctxt "file-type" +msgid "G Code" +msgstr "G Code" + +#: platform/gui.cpp:127 +msgctxt "file-type" +msgid "AutoCAD DXF and DWG files" +msgstr "fichiers AutoCAD DXF et DWG" + +#: platform/gui.cpp:131 +msgctxt "file-type" +msgid "Comma-separated values" +msgstr "CSV (Comma-separated values)" + +#: platform/guigtk.cpp:1434 platform/guimac.mm:1513 platform/guiwin.cpp:1641 +msgid "untitled" +msgstr "sans nom" + +#: platform/guigtk.cpp:1445 platform/guigtk.cpp:1481 platform/guimac.mm:1471 +#: platform/guiwin.cpp:1639 +msgctxt "title" +msgid "Save File" +msgstr "Sauver fichier" + +#: platform/guigtk.cpp:1446 platform/guigtk.cpp:1482 platform/guimac.mm:1454 +#: platform/guiwin.cpp:1645 +msgctxt "title" +msgid "Open File" +msgstr "Ouvrir Fichier" + +#: platform/guigtk.cpp:1449 platform/guigtk.cpp:1488 +msgctxt "button" +msgid "_Cancel" +msgstr "_Annuler" + +#: platform/guigtk.cpp:1450 platform/guigtk.cpp:1486 +msgctxt "button" +msgid "_Save" +msgstr "_Sauver" + +#: platform/guigtk.cpp:1451 platform/guigtk.cpp:1487 +msgctxt "button" +msgid "_Open" +msgstr "_Ouvrir" + +#: solvespace.cpp:175 +msgctxt "title" +msgid "Autosave Available" +msgstr "Sauvegarde automatique existante" + +#: solvespace.cpp:176 +msgctxt "dialog" +msgid "An autosave file is available for this sketch." +msgstr "Un fichier de sauvegarde automatique est disponible pour ce dessin." + +#: solvespace.cpp:177 +msgctxt "dialog" +msgid "Do you want to load the autosave file instead?" +msgstr "Voulez-vous charger la sauvegarde automatique à la place ?" + +#: solvespace.cpp:178 +msgctxt "button" +msgid "&Load autosave" +msgstr "&Charger la sauvegarde automatique" + +#: solvespace.cpp:180 +msgctxt "button" +msgid "Do&n't Load" +msgstr "Ne &pas la charger" + +#: solvespace.cpp:640 +msgctxt "title" +msgid "Modified File" +msgstr "Fichier modifié" + +#: solvespace.cpp:642 +#, c-format +msgctxt "dialog" +msgid "Do you want to save the changes you made to the sketch “%s”?" +msgstr "" +"Voulez-vous sauver les changements que vous avez fait au dessin « %s » ?" + +#: solvespace.cpp:645 +msgctxt "dialog" +msgid "Do you want to save the changes you made to the new sketch?" +msgstr "" +"Voulez-vous sauver les changements que vous avez fait au nouveau dessin ?" + +#: solvespace.cpp:648 +msgctxt "dialog" +msgid "Your changes will be lost if you don't save them." +msgstr "Vos changements seront perdus si vous ne les sauvez pas." + +#: solvespace.cpp:649 +msgctxt "button" +msgid "&Save" +msgstr "&Sauver" + +#: solvespace.cpp:651 +msgctxt "button" +msgid "Do&n't Save" +msgstr "&Ne pas sauver" + +#: solvespace.cpp:672 +msgctxt "title" +msgid "(new sketch)" +msgstr "(nouveau dessin)" + +#: solvespace.cpp:683 +msgctxt "title" +msgid "Property Browser" +msgstr "Navigateur de propriété" + +#: solvespace.cpp:746 +msgid "" +"Constraints are currently shown, and will be exported in the toolpath. This " +"is probably not what you want; hide them by clicking the link at the top of " +"the text window." +msgstr "" +"Les contraintes sont actuellement affichées, et seront exportées dans le " +"toolpath. Ce n'est probablement pas ce que vous voulez, cachez-le en cliquer " +"le lien en haut de la fenêtre de texte." + +#: solvespace.cpp:834 +#, c-format +msgid "" +"Can't identify file type from file extension of filename '%s'; try .dxf or ." +"dwg." +msgstr "" +"Impossible d'identifier le type de fichier à partir de l'extension '%s' ; " +"essayez .dxf ou .dwg." + +#: solvespace.cpp:886 +msgid "Constraint must have a label, and must not be a reference dimension." +msgstr "" +"La contrainte doit avoir un nom, et ne doit pas référencer une dimension." + +#: solvespace.cpp:890 +msgid "Bad selection for step dimension; select a constraint." +msgstr "" +"Mauvaise sélection pour le pas-à-pas dimension ; sélectionnea une contrainte." + +#: solvespace.cpp:914 +msgid "The assembly does not interfere, good." +msgstr "Cet assemblage n'interfère pas, bien." + +#: solvespace.cpp:930 +#, c-format +msgid "" +"The volume of the solid model is:\n" +"\n" +" %s" +msgstr "" +"Le volume du modèle de solide est :\n" +"\n" +" %s" + +#: solvespace.cpp:939 +#, c-format +msgid "" +"\n" +"The volume of current group mesh is:\n" +"\n" +" %s" +msgstr "" +"\n" +"Le volume de ce groupe mesh est :\n" +"\n" +" %s" + +#: solvespace.cpp:944 +msgid "" +"\n" +"\n" +"Curved surfaces have been approximated as triangles.\n" +"This introduces error, typically of around 1%." +msgstr "" +"\n" +"\n" +"Les surfaces courbes ont été approximée par des triangles.\n" +"Cela introduit une erreur, d'environ 1% en général." + +#: solvespace.cpp:959 +#, c-format +msgid "" +"The surface area of the selected faces is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"La surface des faces sélectionnées est :\n" +"\n" +" %s\n" +"\n" +"Les courbes ont été approximée comme étant linéaires par morceau.\n" +"Cela introduit une erreur, d'environ 1%% en général." + +#: solvespace.cpp:968 +msgid "" +"This group does not contain a correctly-formed 2d closed area. It is open, " +"not coplanar, or self-intersecting." +msgstr "" +"Ce groupe ne contient pas de surface 3d fermée correcte. Elle est ouverte, " +"non coplanaire, ou s'intersecte." + +#: solvespace.cpp:980 +#, c-format +msgid "" +"The area of the region sketched in this group is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"La surface de la région dessinée dans ce groupe est :\n" +"\n" +" %s\n" +"\n" +"Les courbes ont été approximée comme linéaires par morceau.\n" +"Cela introduit une erreur, d'environ 1%% en général." + +#: solvespace.cpp:1000 +#, c-format +msgid "" +"The total length of the selected entities is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"La longueur totale des entités sélectionnées est :\n" +"\n" +" %s\n" +"\n" +"\n" +"Les courbes ont été approximée comme linéaires par morceau.\n" +"Cela introduit une erreur, d'environ 1%% en général." + +#: solvespace.cpp:1006 +msgid "Bad selection for perimeter; select line segments, arcs, and curves." +msgstr "" +"Mauvaise sélection pour un périmètre ; sélectionnez des segments de ligne, " +"des arcs, et des coubres." + +#: solvespace.cpp:1022 +msgid "Bad selection for trace; select a single point." +msgstr "Mauvaise sélection pour le tracé ; sélectionnez un unique point." + +#: solvespace.cpp:1049 +#, c-format +msgid "Couldn't write to '%s'" +msgstr "Impossible d'écrire « %s »" + +#: solvespace.cpp:1079 +msgid "The mesh is self-intersecting (NOT okay, invalid)." +msgstr "Le mesh s'intersecte (NON valide)." + +#: solvespace.cpp:1080 +msgid "The mesh is not self-intersecting (okay, valid)." +msgstr "Le mesh ne s'intersecte pas (valide)." + +#: solvespace.cpp:1082 +msgid "The mesh has naked edges (NOT okay, invalid)." +msgstr "Le mesh a des arêtes nues (NON valide)." + +#: solvespace.cpp:1083 +msgid "The mesh is watertight (okay, valid)." +msgstr "Le mesh est étanche (valide)." + +#: solvespace.cpp:1086 +#, c-format +msgid "" +"\n" +"\n" +"The model contains %d triangles, from %d surfaces." +msgstr "" +"\n" +"\n" +"Le modèle contient %d triangles, répartis sur %d surfaces." + +#: solvespace.cpp:1090 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"Zero problematic edges, good.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"Pas d'arête problématique, bien.%s" + +#: solvespace.cpp:1093 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d problematic edges, bad.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d arêtes problématiques, pas bien.%s" + +#: solvespace.cpp:1106 +#, c-format +msgid "" +"This is SolveSpace version %s.\n" +"\n" +"For more information, see http://solvespace.com/\n" +"\n" +"SolveSpace is free software: you are free to modify\n" +"and/or redistribute it under the terms of the GNU\n" +"General Public License (GPL) version 3 or later.\n" +"\n" +"There is NO WARRANTY, to the extent permitted by\n" +"law. For details, visit http://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues and other authors.\n" +msgstr "" +"Ceci est SolveSpace version %s.\n" +"\n" +"Pour plus d'information, visitez http://solvespace.com/\n" +"\n" +"SolveSpace est un logiciel libre : vous êtes libre de le\n" +"modifier et/ou le redistribuer selon les termes de la GNU\n" +"General Public License (GPL) version 3 ou supérieure.\n" +"\n" +"Il n'y a AUCUNE GARANTIE, dans les limites légales.\n" +"Pour plus de détails, visitez http://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues et autres auteurs.\n" + +#: style.cpp:185 +msgid "" +"Can't assign style to an entity that's derived from another entity; try " +"assigning a style to this entity's parent." +msgstr "" +"Impossible d'attribuer le style à une entité dérivée d'une autre entité ; " +"Essayez d'attribuer un style au parent de cette entité." + +#: style.cpp:735 +msgid "Style name cannot be empty" +msgstr "Le nom d'un style ne peut pas être vide" + +#: textscreens.cpp:837 +msgid "Can't repeat fewer than 1 time." +msgstr "Je ne peux pas répéter moins de 1 fois." + +#: textscreens.cpp:841 +msgid "Can't repeat more than 999 times." +msgstr "Je ne peux pas répéter plus de 999 fois." + +#: textscreens.cpp:866 +msgid "Group name cannot be empty" +msgstr "Un nom de groupe ne peut pas être vide" + +#: textscreens.cpp:918 +msgid "Opacity must be between zero and one." +msgstr "L'opacité doit être entre 0 et 1." + +#: textscreens.cpp:953 +msgid "Radius cannot be zero or negative." +msgstr "Le rayon ne peut pas être zéro ou négatif." + +#: toolbar.cpp:18 +msgid "Sketch line segment" +msgstr "Dessin ligne - polyligne" + +#: toolbar.cpp:20 +msgid "Sketch rectangle" +msgstr "Dessin d'un rectangle" + +#: toolbar.cpp:22 +msgid "Sketch circle" +msgstr "Dessin d'un cercle" + +#: toolbar.cpp:24 +msgid "Sketch arc of a circle" +msgstr "Dessin d'un arc de cercle" + +#: toolbar.cpp:26 +msgid "Sketch curves from text in a TrueType font" +msgstr "Dessin de courbes depuis un texte en police TrueType" + +#: toolbar.cpp:28 +msgid "Sketch image from a file" +msgstr "Dessin d'une image depuis un fichier" + +#: toolbar.cpp:30 +msgid "Create tangent arc at selected point" +msgstr "Créer un arc tangent au point sélectionné" + +#: toolbar.cpp:32 +msgid "Sketch cubic Bezier spline" +msgstr "Dessin d'une spline cubique de Bézier" + +#: toolbar.cpp:34 +msgid "Sketch datum point" +msgstr "Dessin d'un point" + +#: toolbar.cpp:36 +msgid "Toggle construction" +msgstr "Basculer en mode « construction »" + +#: toolbar.cpp:38 +msgid "Split lines / curves where they intersect" +msgstr "Diviser lignes / courbes où elles se croisent" + +#: toolbar.cpp:42 +msgid "Constrain distance / diameter / length" +msgstr "Contraindre distance / diamètre / longueur" + +#: toolbar.cpp:44 +msgid "Constrain angle" +msgstr "Contraindre angle" + +#: toolbar.cpp:46 +msgid "Constrain to be horizontal" +msgstr "Contraindre à être horizontal" + +#: toolbar.cpp:48 +msgid "Constrain to be vertical" +msgstr "Contraindre à être vertical" + +#: toolbar.cpp:50 +msgid "Constrain to be parallel or tangent" +msgstr "Contraindre à être parallèle ou tangent" + +#: toolbar.cpp:52 +msgid "Constrain to be perpendicular" +msgstr "Contraindre à être perpendiculaire" + +#: toolbar.cpp:54 +msgid "Constrain point on line / curve / plane / point" +msgstr "Contraindre point sur ligne / courbe / plan / point" + +#: toolbar.cpp:56 +msgid "Constrain symmetric" +msgstr "Contrainte symétrique" + +#: toolbar.cpp:58 +msgid "Constrain equal length / radius / angle" +msgstr "Contrainte égale longueur / rayon / angle" + +#: toolbar.cpp:60 +msgid "Constrain normals in same orientation" +msgstr "Contrainte normales dans la même direction" + +#: toolbar.cpp:62 +msgid "Other supplementary angle" +msgstr "Autre angle supplémentaire" + +#: toolbar.cpp:64 +msgid "Toggle reference dimension" +msgstr "Basculer cote maîtresse / cote indicative" + +#: toolbar.cpp:68 +msgid "New group extruding active sketch" +msgstr "Nouveau groupe d'extrusion du dessin actif" + +#: toolbar.cpp:70 +msgid "New group rotating active sketch" +msgstr "Nouveau groupe de révolution du dessin actif (tour complet)" + +#: toolbar.cpp:72 +msgid "New group helix from active sketch" +msgstr "Nouveau groupe hélice du dessin actif" + +#: toolbar.cpp:74 +msgid "New group revolve active sketch" +msgstr "Nouveau groupe de révolution partielle du dessin actif" + +#: toolbar.cpp:76 +msgid "New group step and repeat rotating" +msgstr "Nouveau groupe de répétition circulaire" + +#: toolbar.cpp:78 +msgid "New group step and repeat translating" +msgstr "Nouveau groupe de répétition linéaire" + +#: toolbar.cpp:80 +msgid "New group in new workplane (thru given entities)" +msgstr "" +"Nouveau groupe dans un nouveau plan de travail (Par des entités données)" + +#: toolbar.cpp:82 +msgid "New group in 3d" +msgstr "Nouveau groupe en 3d" + +#: toolbar.cpp:84 +msgid "New group linking / assembling file" +msgstr "Nouveau groupe lié / assemblage" + +#: toolbar.cpp:88 +msgid "Nearest isometric view" +msgstr "Vue isométrique la plus proche" + +#: toolbar.cpp:90 +msgid "Align view to active workplane" +msgstr "Aligner la vue sur le plan de travail actif" + +#: util.cpp:165 +msgctxt "title" +msgid "Error" +msgstr "Erreur" + +#: util.cpp:165 +msgctxt "title" +msgid "Message" +msgstr "Message" + +#: util.cpp:170 +msgctxt "button" +msgid "&OK" +msgstr "&Ok" + +#: view.cpp:127 +msgid "Scale cannot be zero or negative." +msgstr "L'échelle ne peut pas être zéro ou négative." + +#: view.cpp:139 view.cpp:148 +msgid "Bad format: specify x, y, z" +msgstr "Mauvais format : Spécifiez x, y, z" + +#~ msgid "" +#~ "Bad selection for on point / curve / plane constraint. This constraint " +#~ "can apply to:\n" +#~ "\n" +#~ " * two points (points coincident)\n" +#~ " * a point and a workplane (point in plane)\n" +#~ " * a point and a line segment (point on line)\n" +#~ " * a point and a circle or arc (point on curve)\n" +#~ " * a point and a plane face (point on face)\n" +#~ msgstr "" +#~ "Mauvaise sélection pour la contrainte point / courbe / plan. Cette " +#~ "contrainte peut s'appliquer à :\n" +#~ "\n" +#~ " – Deux points (points coïncidents)\n" +#~ " – Un point et un plan de travail (point dans le plan)\n" +#~ " – Un point et un segment de ligne (point en ligne)\n" +#~ " – Un point et un cercle ou un arc (point sur courbe)\n" +#~ " – Un point et une face plane (point sur une face)\n" + +#~ msgid "" +#~ "Bad selection for equal length / radius constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two line segments (equal length)\n" +#~ " * two line segments and two points (equal point-line distances)\n" +#~ " * a line segment and two points (equal point-line distances)\n" +#~ " * a line segment, and a point and line segment (point-line distance " +#~ "equals length)\n" +#~ " * four line segments or normals (equal angle between A,B and C,D)\n" +#~ " * three line segments or normals (equal angle between A,B and B,C)\n" +#~ " * two circles or arcs (equal radius)\n" +#~ " * a line segment and an arc (line segment length equals arc length)\n" +#~ msgstr "" +#~ "Mauvaise sélection pour une contrainte de longueur / rayon égale. Cette " +#~ "contrainte peut s'appliquer à :\n" +#~ "\n" +#~ " – Deux segments de ligne (longueur égale)\n" +#~ " – Deux segments de ligne et deux points (distances point-ligne " +#~ "égales)\n" +#~ " – Un segment de ligne et deux points (distances point-ligne égales)\n" +#~ " – Un segment de ligne ou un segment de ligne et point (distance point-" +#~ "ligne de longueur égale)\n" +#~ " – Quatre segments de ligne ou des normales (angle entre A, B et C, D " +#~ "égaux)\n" +#~ " – Trois segments de ligne ou des normales (angle entre A, B et B, C " +#~ "égaux)\n" +#~ " – Deux cercles ou arcs (rayon égaux)\n" +#~ " – Un segment de ligne et un arc (la longueur de segment de ligne est " +#~ "égale à la longueur d'arc)\n" + +#~ msgid "" +#~ "Bad selection for horizontal / vertical constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two points\n" +#~ " * a line segment\n" +#~ msgstr "" +#~ "Mauvaise sélection pour la contrainte horizontale / verticale. Cette " +#~ "contrainte peut s'appliquer à :\n" +#~ "\n" +#~ " – deux points\n" +#~ " – Un segment de ligne\n" + +#~ msgid "" +#~ "Bad selection for angle constraint. This constraint can apply to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ " * a line segment and a normal\n" +#~ " * two normals\n" +#~ msgstr "" +#~ "Mauvaise sélection pour une contrainte d'angle. Cette contrainte peut " +#~ "s'appliquer à :\n" +#~ "\n" +#~ " – Deux segments de ligne\n" +#~ " – Un segment de ligne et une normale\n" +#~ " – Deux normales\n" + +#~ msgid "" +#~ "Bad selection for parallel / tangent constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two line segments (parallel)\n" +#~ " * a line segment and a normal (parallel)\n" +#~ " * two normals (parallel)\n" +#~ " * two line segments, arcs, or beziers, that share an endpoint " +#~ "(tangent)\n" +#~ msgstr "" +#~ "Mauvaise sélection pour la contrainte parallèle / tangente. Cette " +#~ "contrainte peut s'appliquer à :\n" +#~ "\n" +#~ " – Deux segments de ligne (parallèles)\n" +#~ " – Un segment de ligne et un parallèle (parallèle)\n" +#~ " – Deux normales (parallèles)\n" +#~ " – Deux segments de ligne, des arcs ou des Béziers, qui partagent une " +#~ "extrémité (tangente)\n" + +#~ msgid "" +#~ "Bad selection for perpendicular constraint. This constraint can apply " +#~ "to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ " * a line segment and a normal\n" +#~ " * two normals\n" +#~ msgstr "" +#~ "Mauvaise sélection pour une contrainte perpendiculaire. Cette contrainte " +#~ "peut s'appliquer à :\n" +#~ "\n" +#~ " – Deux segments de ligne\n" +#~ " – Un segment de ligne et une normale\n" +#~ " – Deux normales\n" + +#~ msgid "A&ngle" +#~ msgstr "A&ngle" + +#~ msgid "E&qual Length / Radius / Angle" +#~ msgstr "É&galité Longueur / Rayon / Angle" + +#~ msgid "" +#~ "Bad selection for length ratio constraint. This constraint can apply to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ msgstr "" +#~ "Mauvaise sélection pour la contrainte du rapport de longueur. Cette " +#~ "contrainte peut s'appliquer à :\n" +#~ "\n" +#~ "    * Deux segments de ligne\n" + +#~ msgid "" +#~ "Bad selection for length difference constraint. This constraint can apply " +#~ "to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ msgstr "" +#~ "Mauvaise sélection pour la contrainte de différence de longueur. Cette " +#~ "contrainte peut s'appliquer à :\n" +#~ "\n" +#~ "    * Deux segments de ligne\n" + +#~ msgid "Length Ra&tio" +#~ msgstr "R&apport de Longueur" + +#~ msgid "Length Diff&erence" +#~ msgstr "D&ifférence de Longueur" + +#~ msgid "" +#~ "Bad selection for new sketch in workplane. This group can be created " +#~ "with:\n" +#~ "\n" +#~ " * a point (through the point, orthogonal to coordinate axes)\n" +#~ " * a point and two line segments (through the point, parallel to the " +#~ "lines)\n" +#~ " * a workplane (copy of the workplane)\n" +#~ msgstr "" +#~ "Mauvaise sélection pour un nouveau dessin dans le plan de travail. Ce " +#~ "groupe peut être créé avec :\n" +#~ "\n" +#~ "    * Un point (par le point, orthogonal aux axes de coordonnées)\n" +#~ "    * Un point et deux segments de ligne (par le point, parallèle aux " +#~ "lignes)\n" +#~ "    * Un plan de travail (copie du plan de travail)\n" + +#~ msgid "Specify between 0 and 8 digits after the decimal." +#~ msgstr "Spécifiez entre 0 et 8 chiffres après la virgule." + +#~ msgid "Show Degrees of &Freedom" +#~ msgstr "Montrer les Degrés de &Liberté" + +#~ msgid "click to place bottom left of text" +#~ msgstr "cliquez pour placer le bas gauche du texte" + +#~ msgid "Do you want to save the changes you made to the new sketch?" +#~ msgstr "" +#~ "Voulez-vous enregistrer les modifications que vous avez apportées au " +#~ "nouveau dessin ?" + +#~ msgid "Your changes will be lost if you don't save them." +#~ msgstr "Vos modifications seront perdues si vous ne les enregistrez pas." + +#~ msgctxt "button" +#~ msgid "Save" +#~ msgstr "Sauver" + +#~ msgctxt "button" +#~ msgid "Cancel" +#~ msgstr "Annuler" + +#~ msgctxt "button" +#~ msgid "Don't Save" +#~ msgstr "Ne pas sauver" + +#~ msgid "Do you want to load the autosave file instead?" +#~ msgstr "Voulez-vous charger le fichier de sauvegarde à la place ?" + +#~ msgctxt "button" +#~ msgid "Load" +#~ msgstr "Charger" + +#~ msgctxt "button" +#~ msgid "Don't Load" +#~ msgstr "Ne pas charger" + +#~ msgid "" +#~ "Do you want to locate it manually?\n" +#~ "If you select “No”, any geometry that depends on the missing file will be " +#~ "removed." +#~ msgstr "" +#~ "Voulez-vous le localiser manuellement ?\n" +#~ "Si vous sélectionnez « Non », toute géométrie qui dépend du fichier " +#~ "manquant sera supprimée." + +#~ msgctxt "button" +#~ msgid "Yes" +#~ msgstr "Oui" + +#~ msgctxt "button" +#~ msgid "No" +#~ msgstr "Non" + +#~ msgctxt "button" +#~ msgid "OK" +#~ msgstr "Valider" + +#~ msgid "_Cancel" +#~ msgstr "_Annuler" + +#~ msgid "_Open" +#~ msgstr "_Ouvrir" + +#~ msgid "" +#~ "The file has changed since it was last saved.\n" +#~ "\n" +#~ "Do you want to save the changes?" +#~ msgstr "" +#~ "Le fichier a changé depuis sa dernière sauvegarde.\n" +#~ "\n" +#~ "Voulez-vous enregistrer les modifications ?" + +#~ msgctxt "button" +#~ msgid "Do_n't Save" +#~ msgstr "_Ne pas sauver" + +#~ msgctxt "button" +#~ msgid "_Load autosave" +#~ msgstr "_Charger la sauvegarde automatique" + +#~ msgctxt "button" +#~ msgid "Do_n't Load" +#~ msgstr "_Ne pas charger" + +#~ msgctxt "button" +#~ msgid "_Yes" +#~ msgstr "_Oui" + +#~ msgctxt "button" +#~ msgid "_No" +#~ msgstr "_Non" + +#~ msgid "SolveSpace models" +#~ msgstr "Modèles SolveSpace" + +#~ msgid "PNG file" +#~ msgstr "Fichier PNG" + +#~ msgid "STL mesh" +#~ msgstr "Maillage STL" + +#~ msgid "Wavefront OBJ mesh" +#~ msgstr "Maillage Wavefront OBJ" + +#~ msgid "Three.js-compatible mesh, with viewer" +#~ msgstr "Maillage Three-js-compatible, avec visualisateur" + +#~ msgid "Three.js-compatible mesh, mesh only" +#~ msgstr "Maillage Three-js-compatible, maillage seul" + +#~ msgid "STEP file" +#~ msgstr "Fichier STEP" + +#~ msgid "PDF file" +#~ msgstr "Fichier PDF" + +#~ msgid "Encapsulated PostScript" +#~ msgstr "Encapsulated PostScript" + +#~ msgid "Scalable Vector Graphics" +#~ msgstr "Scalable Vector Graphics" + +#~ msgid "DXF file (AutoCAD 2007)" +#~ msgstr "Fichier DXF (AutoCAD 2007)" + +#~ msgid "HPGL file" +#~ msgstr "Fichier HPGL" + +#~ msgid "G Code" +#~ msgstr "G Code" + +#~ msgid "AutoCAD DXF and DWG files" +#~ msgstr "Fichiers AutoCAD DXF et DWG" + +#~ msgid "Comma-separated values" +#~ msgstr "Valeurs CSV séparées par des virgules" diff --git a/res/locales/ja_JP.po b/res/locales/ja_JP.po new file mode 100644 index 0000000..211c43c --- /dev/null +++ b/res/locales/ja_JP.po @@ -0,0 +1,2384 @@ +# Japanese translations for SolveSpace package. +# Copyright (C) 2022 the Solvespace authors +# This file is distributed under the same license as the SolveSpace package. +# verylowfreq, 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: SolveSpace 3.0\n" +"Report-Msgid-Bugs-To: phkahler@gmail.com\n" +"POT-Creation-Date: 2025-01-26 21:04+0200\n" +"PO-Revision-Date: 2025-02-01 10:31+0900\n" +"Last-Translator: https://github.com/verylowfreq\n" +"Language-Team: \n" +"Language: ja_JP\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.5\n" + +#: clipboard.cpp:314 +msgid "" +"Cut, paste, and copy work only in a workplane.\n" +"\n" +"Activate one with Sketch -> In Workplane." +msgstr "" +"切り取り・貼り付け・コピーは作業平面でのみ利用できます。\n" +"\n" +"作業平面は「スケッチ -> 作業平面上でスケッチする」から指定してください。" + +#: clipboard.cpp:331 +msgid "Clipboard is empty; nothing to paste." +msgstr "クリップボードは空です。貼り付けるものはありません。" + +#: clipboard.cpp:378 +msgid "Number of copies to paste must be at least one." +msgstr "貼り付ける個数は1以上でなければなりません。" + +#: clipboard.cpp:394 textscreens.cpp:879 +msgid "Scale cannot be zero." +msgstr "縮尺は0にできません。" + +#: clipboard.cpp:436 +msgid "Select one point to define origin of rotation." +msgstr "回転の原点を決めるために、点を1つ選択してください。" + +#: clipboard.cpp:448 +msgid "Select two points to define translation vector." +msgstr "移動方向を決めるために、点を2つ選択してください。" + +#: clipboard.cpp:458 +msgid "" +"Transformation is identity. So all copies will be exactly on top of each " +"other." +msgstr "移動量が 0 です。すべての複製は同一位置にあります。" + +#: clipboard.cpp:462 +msgid "Too many items to paste; split this into smaller pastes." +msgstr "複製の数が多すぎます。分割してください。" + +#: clipboard.cpp:467 +msgid "No workplane active." +msgstr "作業平面がアクティブでありません。" + +#: confscreen.cpp:410 +msgid "Bad format: specify coordinates as x, y, z" +msgstr "書式が誤っています。座標を \"x, y, z\" で指定してください。" + +#: confscreen.cpp:420 style.cpp:729 textscreens.cpp:910 +msgid "Bad format: specify color as r, g, b" +msgstr "書式が誤っています。色を \"r, g, b\" で指定してください。" + +#: confscreen.cpp:446 +msgid "" +"The perspective factor will have no effect until you enable View -> Use " +"Perspective Projection." +msgstr "" +"透視投影の拡大率は、「表示 -> 透視投影で表示」を有効にするまで反映されませ" +"ん。" + +#: confscreen.cpp:464 confscreen.cpp:474 +#, c-format +msgid "Specify between 0 and %d digits after the decimal." +msgstr "小数点以下の桁数は、0 から %d の範囲で指定してください。" + +#: confscreen.cpp:486 +msgid "Export scale must not be zero!" +msgstr "エクスポートの比率には0を指定できません。" + +#: confscreen.cpp:498 +msgid "Cutter radius offset must not be negative!" +msgstr "ドリル径は負の値にできません。" + +#: confscreen.cpp:557 +msgid "Bad value: autosave interval should be positive" +msgstr "値が誤っています。オートセーブの間隔は正の値を指定してください。" + +#: confscreen.cpp:560 +msgid "Bad format: specify interval in integral minutes" +msgstr "書式が誤っています。間隔は分単位で整数を指定してください。" + +#: constraint.cpp:12 +msgctxt "constr-name" +msgid "pts-coincident" +msgstr "" + +#: constraint.cpp:13 +msgctxt "constr-name" +msgid "pt-pt-distance" +msgstr "" + +#: constraint.cpp:14 +msgctxt "constr-name" +msgid "pt-line-distance" +msgstr "" + +#: constraint.cpp:15 +msgctxt "constr-name" +msgid "pt-plane-distance" +msgstr "" + +#: constraint.cpp:16 +msgctxt "constr-name" +msgid "pt-face-distance" +msgstr "" + +#: constraint.cpp:17 +msgctxt "constr-name" +msgid "proj-pt-pt-distance" +msgstr "" + +#: constraint.cpp:18 +msgctxt "constr-name" +msgid "pt-in-plane" +msgstr "" + +#: constraint.cpp:19 +msgctxt "constr-name" +msgid "pt-on-line" +msgstr "" + +#: constraint.cpp:20 +msgctxt "constr-name" +msgid "pt-on-face" +msgstr "" + +#: constraint.cpp:21 +msgctxt "constr-name" +msgid "eq-length" +msgstr "" + +#: constraint.cpp:22 +msgctxt "constr-name" +msgid "eq-length-and-pt-ln-dist" +msgstr "" + +#: constraint.cpp:23 +msgctxt "constr-name" +msgid "eq-pt-line-distances" +msgstr "" + +#: constraint.cpp:24 +msgctxt "constr-name" +msgid "length-ratio" +msgstr "" + +#: constraint.cpp:25 +msgctxt "constr-name" +msgid "arc-arc-length-ratio" +msgstr "" + +#: constraint.cpp:26 +msgctxt "constr-name" +msgid "arc-line-length-ratio" +msgstr "" + +#: constraint.cpp:27 +msgctxt "constr-name" +msgid "length-difference" +msgstr "" + +#: constraint.cpp:28 +msgctxt "constr-name" +msgid "arc-arc-len-difference" +msgstr "" + +#: constraint.cpp:29 +msgctxt "constr-name" +msgid "arc-line-len-difference" +msgstr "" + +#: constraint.cpp:30 +msgctxt "constr-name" +msgid "symmetric" +msgstr "" + +#: constraint.cpp:31 +msgctxt "constr-name" +msgid "symmetric-h" +msgstr "" + +#: constraint.cpp:32 +msgctxt "constr-name" +msgid "symmetric-v" +msgstr "" + +#: constraint.cpp:33 +msgctxt "constr-name" +msgid "symmetric-line" +msgstr "" + +#: constraint.cpp:34 +msgctxt "constr-name" +msgid "at-midpoint" +msgstr "" + +#: constraint.cpp:35 +msgctxt "constr-name" +msgid "horizontal" +msgstr "" + +#: constraint.cpp:36 +msgctxt "constr-name" +msgid "vertical" +msgstr "" + +#: constraint.cpp:37 +msgctxt "constr-name" +msgid "diameter" +msgstr "" + +#: constraint.cpp:38 +msgctxt "constr-name" +msgid "pt-on-circle" +msgstr "" + +#: constraint.cpp:39 +msgctxt "constr-name" +msgid "same-orientation" +msgstr "" + +#: constraint.cpp:40 +msgctxt "constr-name" +msgid "angle" +msgstr "" + +#: constraint.cpp:41 +msgctxt "constr-name" +msgid "parallel" +msgstr "" + +#: constraint.cpp:42 +msgctxt "constr-name" +msgid "arc-line-tangent" +msgstr "" + +#: constraint.cpp:43 +msgctxt "constr-name" +msgid "cubic-line-tangent" +msgstr "" + +#: constraint.cpp:44 +msgctxt "constr-name" +msgid "curve-curve-tangent" +msgstr "" + +#: constraint.cpp:45 +msgctxt "constr-name" +msgid "perpendicular" +msgstr "" + +#: constraint.cpp:46 +msgctxt "constr-name" +msgid "eq-radius" +msgstr "" + +#: constraint.cpp:47 +msgctxt "constr-name" +msgid "eq-angle" +msgstr "" + +#: constraint.cpp:48 +msgctxt "constr-name" +msgid "eq-line-len-arc-len" +msgstr "" + +#: constraint.cpp:49 +msgctxt "constr-name" +msgid "lock-where-dragged" +msgstr "" + +#: constraint.cpp:50 +msgctxt "constr-name" +msgid "comment" +msgstr "" + +#: constraint.cpp:151 +msgid "" +"The point you selected does not belong to the arc. The arc and line segment " +"do not share an end point.\n" +"\n" +"Select the end point of the arc at which you want it to be tangent to the " +"line." +msgstr "" +"選択された点は円弧上にありません。円弧と直線が同じ端点を共有していません。\n" +"\n" +"線分に正接させたい円弧の端点を選択してください。" + +#: constraint.cpp:158 +msgid "" +"The tangent arc and line segment must share an endpoint. Constrain them with " +"Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the arc at which you want it to be " +"tangent to the line." +msgstr "" +"正接する円弧と線分は端点を共有している必要があります。正接拘束をする前に「拘" +"束 -> 一致」で拘束してください。\n" +"\n" +"もしくは、線分に正接させたい円弧の端点を選択してください。" + +#: constraint.cpp:186 +msgid "" +"The point you selected is not an end point of the cubic spline. The spline " +"and line segment do not share an end point.\n" +"\n" +"Select the end point of the spline at which you want it to be tangent to the " +"line." +msgstr "" +"選択された点は三次スプラインの端点ではありません。スプラインと線分が同じ端点" +"を共有していません。\n" +"\n" +"線分に正接させたいスプラインの端点を選択してください。" + +#: constraint.cpp:193 +msgid "" +"The tangent cubic spline and line segment must share an endpoint. Constrain " +"them with Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the cubic spline at which you want it " +"to be tangent to the line." +msgstr "" +"ベジェ曲線と線分は端点を共有している必要があります。正接拘束をする前に「拘束 " +"-> 一致」で拘束してください。\n" +"\n" +"もしくは、線分に正接させたいベジェ曲線の端点を選択してください。" + +#: constraint.cpp:237 +msgid "" +"The points you selected are not end points of the two curves. The curves do " +"not share an end point.\n" +"\n" +"Select the end points of both curves at which you want them to be tangent to " +"each other." +msgstr "" +"選択された点は2つのカーブの端点ではありません。カーブが同じ端点を共有していま" +"せん。\n" +"\n" +"互いに正接させたいカーブの共通の端点を選択してください。" + +#: constraint.cpp:244 +msgid "" +"The curves must share an endpoint. Constrain them with Constrain -> On Point " +"before constraining tangent.\n" +"\n" +"Alternatively select the end points of both curves at which you want the " +"curves to be tangent." +msgstr "" +"曲線同士は端点を共有している必要があります。正接拘束をする前に「拘束 -> 一" +"致」で拘束してください。\n" +"\n" +"もしくは、正接させたい曲線の共通の端点を選択してください。" + +#: constraint.cpp:303 +msgid "" +"Bad selection for distance / diameter constraint. This constraint can apply " +"to:\n" +"\n" +" * two points (distance between points)\n" +" * a line segment (length)\n" +" * two points and a line segment or normal (projected distance)\n" +" * a workplane and a point (minimum distance)\n" +" * a line segment and a point (minimum distance)\n" +" * a plane face and a point (minimum distance)\n" +" * a circle or an arc (diameter)\n" +msgstr "" +"距離・直径拘束ができませんでした。次の要素に適用できます:\n" +"\n" +" * 2 つの点 (点同士の距離)\n" +" * 1 つの線分 (長さ)\n" +" * 2 つの点と、1 つの線分もしくは法線 (投影された距離)\n" +" * 1 つの作業平面と 1 つの点 (最小の距離)\n" +" * 1 つの線分と 1 つの点 (最小の距離)\n" +" * 1 つの平面フェイスと1つの点 (最小の距離)\n" +" * 1 つの円もしくは円弧 (直径)\n" +"\n" + +#: constraint.cpp:366 +msgid "" +"Bad selection for on point / curve / plane constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points (points coincident)\n" +" * a point and a workplane (point in plane)\n" +" * a point and a line segment (point on line)\n" +" * a point and a circle or arc (point on curve)\n" +" * a point and one to three plane faces (point on face(s))\n" +msgstr "" +"一致拘束ができませんでした。次の要素に適用できます。\n" +"\n" +" * 2 つの点 (点を同一位置に)\n" +" * 1 つの点と 1 つの作業平面 (点を平面上に)\n" +" * 1 つの点と 1 つの線分 (点を直線状に)\n" +" * 1 つの点と、1つの円もしくは円弧 (点を曲線上に)\n" +" * 1 つの点と、1 から3つの平面フェイス (点をフェイス上に)\n" + +#: constraint.cpp:427 +msgid "" +"Bad selection for equal length / radius constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more line segments (equal length)\n" +" * two line segments and two points (equal point-line distances)\n" +" * a line segment and two points (equal point-line distances)\n" +" * a line segment, and a point and line segment (point-line distance " +"equals length)\n" +" * two or more circles or arcs (equal radius)\n" +" * a line segment and an arc (line segment length equals arc length)\n" +msgstr "" +"長さ・半径の同値拘束ができませんでした。次の要素に適用できます:\n" +"\n" +" * 2 つ以上の線分 (長さを同じに)\n" +" * 2 つの線分と 2 つの点 (それぞれ直線と点の距離を同じに)\n" +" * 1 つの線分と 2 つの点 (直線とそれぞれの点の距離を同じに)\n" +" * 1 つの線分と、1 つの点と 1 つの線分 (線分の長さと、直線と点の距離を同じ" +"に)\n" +" * 2 つ以上の円もしくは円弧 (半径を同じに)\n" +" * 1 つの線分と 1 つの円弧 (線分の長さと円弧の長さを同じに)\n" + +#: constraint.cpp:480 +msgid "" +"Bad selection for length ratio constraint. This constraint can apply to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"長さの比率の拘束ができませんでした。次の要素に適用できます:\n" +"\n" +" * 2 つの線分\n" +" * 2 つの円弧\n" +" * 円弧と線分\n" + +#: constraint.cpp:515 +msgid "" +"Bad selection for length difference constraint. This constraint can apply " +"to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"長さの差の拘束ができませんでした。次の要素に適用できます:\n" +"\n" +" * 2 つの線分\n" +" * 2 つの円弧\n" +" * 円弧と線分\n" + +#: constraint.cpp:550 +msgid "" +"Bad selection for at midpoint constraint. This constraint can apply to:\n" +"\n" +" * a line segment and a point (point at midpoint)\n" +" * a line segment and a workplane (line's midpoint on plane)\n" +msgstr "" +"中点拘束できませんでした。次の要素に適用できます:\n" +"\n" +" * 線分と点 (点を直線の中点に置きます)\n" +" * 線分と作業平面 (直線の中点を作業平面上に置きます)\n" + +#: constraint.cpp:608 +msgid "" +"Bad selection for symmetric constraint. This constraint can apply to:\n" +"\n" +" * two points or a line segment (symmetric about workplane's coordinate " +"axis)\n" +" * line segment, and two points or a line segment (symmetric about line " +"segment)\n" +" * workplane, and two points or a line segment (symmetric about " +"workplane)\n" +msgstr "" +"対象拘束ができませんでした。次の要素の適用できます:\n" +"\n" +" * 2 つの点、もしくは 1 つの線分 (作業平面の座標軸について対称をとります)\n" +" * 線分と、2 つの点もしくは 1 つの線分 (線分について対称をとります)\n" +" * 作業平面と、2 つの点もしくは 1 つの線分 (作業平面について対称をとりま" +"す)\n" + +#: constraint.cpp:623 +msgid "" +"A workplane must be active when constraining symmetric without an explicit " +"symmetry plane." +msgstr "" +"明示的に対称平面を指定しない場合は、対象拘束をするには作業平面をアクティブに" +"してください。" + +#: constraint.cpp:663 +msgid "" +"Activate a workplane (with Sketch -> In Workplane) before applying a " +"horizontal or vertical constraint." +msgstr "" +"垂直拘束や水平拘束をする前に、「スケッチ -> 作業平面上でスケッチする」で作業" +"平面をアクティブにしてください。" + +#: constraint.cpp:679 +msgid "" +"Bad selection for horizontal / vertical constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points\n" +" * one or more line segments\n" +msgstr "" +"垂直・水平拘束ができませんでした。次の要素に適用できます:\n" +"\n" +" * 2つ以上の点\n" +" * 1つ以上の線分\n" + +#: constraint.cpp:697 +msgid "" +"Bad selection for same orientation constraint. This constraint can apply " +"to:\n" +"\n" +" * two normals\n" +msgstr "" +"同じ方向の拘束ができませんでした。次の要素に適用できます。\n" +"\n" +" * 2 つの法線\n" + +#: constraint.cpp:748 +msgid "Must select an angle constraint." +msgstr "角度拘束を選択してください。" + +#: constraint.cpp:761 +msgid "Must select a constraint with associated label." +msgstr "値を持つ拘束を選択してください。" + +#: constraint.cpp:784 +msgid "" +"Bad selection for angle constraint. This constraint can apply to:\n" +"\n" +"Angle between:\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +"\n" +"Equal angles:\n" +" * four line segments or normals (equal angle between A,B and C,D)\n" +" * three line segments or normals (equal angle between A,B and B,C)\n" +msgstr "" +"角度拘束ができませんでした。\n" +"\n" +"次の要素間の角度に適用できます:\n" +" * 2つの線分\n" +" * 1つの線分と1つの法線\n" +" * 2つの法線\n" +"\n" +"次の要素を同じ角度に拘束できます:\n" +" * 4つの線分もしくは法線 (AとB、CとDの角度を同じにする)\n" +" * 3つの線分もしくは法線 (AとB、BとCの角度を同じにする)\n" + +#: constraint.cpp:872 +msgid "Curve-curve tangency must apply in workplane." +msgstr "曲線同士の正接は作業平面上で適用できます。" + +#: constraint.cpp:887 +msgid "" +"Bad selection for parallel / tangent constraint. This constraint can apply " +"to:\n" +"\n" +" * two faces\n" +" * two or more line segments (parallel)\n" +" * one or more line segments and one or more normals (parallel)\n" +" * two or more normals (parallel)\n" +" * two line segments, arcs, or beziers, that share an endpoint (tangent)\n" +" * two line segments, arcs, or beziers, that do not share an endpoint and " +"the end point(s) of the curve(s) (tangent)\n" +msgstr "" +"平行・正接拘束ができませんでした。次の要素に適用できます:\n" +"\n" +" * 2つのフェイス\n" +" * 2つ以上の線分 (平行に)\n" +" * 1つ以上の線分と1つ以上の法線 (平行に)\n" +" * 2つ以上の法線 (平行に)\n" +" * 端点の1つが一致する、2つの線分か円弧かベジェ曲線 (正接に)\n" +" * 正接する曲線と端点を共有しない、2つの線分か円弧かベジェ曲線\n" + +#: constraint.cpp:914 +msgid "" +"Bad selection for perpendicular constraint. This constraint can apply to:\n" +"\n" +" * two faces\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +msgstr "" +"垂線拘束ができませんでした。次の要素に適用できます:\n" +"\n" +" * 2つのフェイス\n" +" * 2つの線分\n" +" * 1つの線分と1つの法線\n" +" * 2つの法線\n" + +#: constraint.cpp:931 +msgid "" +"Bad selection for lock point where dragged constraint. This constraint can " +"apply to:\n" +"\n" +" * a point\n" +msgstr "" +"点の位置の拘束 (点を他のオブジェクトに連動して動かさない) ができませんでし" +"た。次の要素に適用できます:\n" +"\n" +" * 1 つの点\n" + +#: constraint.cpp:946 mouse.cpp:1160 +msgid "NEW COMMENT -- DOUBLE-CLICK TO EDIT" +msgstr "(コメント - ダブルクリックで編集)" + +#: constraint.cpp:952 +msgid "click center of comment text" +msgstr "コメントテキストの中心をクリックで指定" + +#: export.cpp:19 +msgid "" +"No solid model present; draw one with extrudes and revolves, or use Export " +"2d View to export bare lines and curves." +msgstr "" +"ソリッドモデルがありません。押し出しや周回、回転を利用するか、平面への投影視" +"点のエクスポートで直線や曲線をそのまま出力します。" + +#: export.cpp:61 +msgid "" +"Bad selection for export section. Please select:\n" +"\n" +" * nothing, with an active workplane (workplane is section plane)\n" +" * a face (section plane through face)\n" +" * a point and two line segments (plane through point and parallel to " +"lines)\n" +msgstr "" +"2D断面の出力ができませんでした。次のように選択してください:\n" +"\n" +" * なにも選択せず、作業平面がアクティブである (作業平面を断面とします)\n" +" * フェイス (フェイスを通る断面を用います)\n" +" * 1 つの点と 2 つの線分 (点を通り直線と平行する断面を用います)\n" + +#: export.cpp:818 +msgid "Active group mesh is empty; nothing to export." +msgstr "" +"アクティブなグループのメッシュは空です。エクスポートは行なわれませんでした。" + +#: exportvector.cpp:336 +msgid "freehand lines were replaced with continuous lines" +msgstr "フリーハンドの線は実線に置き換えられました。" + +#: exportvector.cpp:338 +msgid "zigzag lines were replaced with continuous lines" +msgstr "ジグザグ線は実線に置き換えられました。" + +#: exportvector.cpp:592 +msgid "" +"Some aspects of the drawing have no DXF equivalent and were not exported:\n" +msgstr "" +"図面のうち一部の側面は、対応するものがDXFにないため、それらは出力されませんで" +"した:\n" + +#: exportvector.cpp:838 +msgid "" +"PDF page size exceeds 200 by 200 inches; many viewers may reject this file." +msgstr "" +"PDFのページ寸法が 200x200 インチを超えています。多くのPDFリーダーでは正常に読" +"み込めません。" + +#: file.cpp:44 group.cpp:91 +msgctxt "group-name" +msgid "sketch-in-plane" +msgstr "" + +#: file.cpp:62 +msgctxt "group-name" +msgid "#references" +msgstr "" + +#: file.cpp:555 +msgid "The file is empty. It may be corrupt." +msgstr "ファイルが空です。ファイルが破損している可能性があります。" + +#: file.cpp:560 +msgid "" +"Unrecognized data in file. This file may be corrupt, or from a newer version " +"of the program." +msgstr "" +"ファイルに処理できないデータが含まれています。ファイルが破損しているか、より" +"新しいバージョンで作成されたファイルの可能性があります。" + +#: file.cpp:876 +msgctxt "title" +msgid "Missing File" +msgstr "不明ファイル" + +#: file.cpp:877 +#, c-format +msgctxt "dialog" +msgid "The linked file “%s” is not present." +msgstr "リンクされたファイル \"%s\" が見つかりません。" + +#: file.cpp:879 +msgctxt "dialog" +msgid "" +"Do you want to locate it manually?\n" +"\n" +"If you decline, any geometry that depends on the missing file will be " +"permanently removed." +msgstr "" +"手動で配置しますか?\n" +"\n" +"キャンセルした場合、不明なファイルに依存しているすべての寸法は削除されます。" + +#: file.cpp:882 +msgctxt "button" +msgid "&Yes" +msgstr "はい (&Y)" + +#: file.cpp:884 +msgctxt "button" +msgid "&No" +msgstr "いいえ (&N)" + +#: file.cpp:886 solvespace.cpp:652 +msgctxt "button" +msgid "&Cancel" +msgstr "キャンセル (&C)" + +#: graphicswin.cpp:41 +msgid "&File" +msgstr "ファイル (&F)" + +#: graphicswin.cpp:42 +msgid "&New" +msgstr "新規作成 (&N)" + +#: graphicswin.cpp:43 +msgid "&Open..." +msgstr "開く... (&O)" + +#: graphicswin.cpp:44 +msgid "Open &Recent" +msgstr "最近使用したファイル (&R)" + +#: graphicswin.cpp:45 +msgid "&Save" +msgstr "保存 (&S)" + +#: graphicswin.cpp:46 +msgid "Save &As..." +msgstr "名前を付けて保存... (&A)" + +#: graphicswin.cpp:48 +msgid "Export &Image..." +msgstr "画像をエクスポート... (&I)" + +#: graphicswin.cpp:49 +msgid "Export 2d &View..." +msgstr "平面への投影視点をエクスポート... (&V)" + +#: graphicswin.cpp:50 +msgid "Export 2d &Section..." +msgstr "2D断面をエクスポート... (&S)" + +#: graphicswin.cpp:51 +msgid "Export 3d &Wireframe..." +msgstr "三次元ワイヤーフレームをエクスポート... (&W)" + +#: graphicswin.cpp:52 +msgid "Export Triangle &Mesh..." +msgstr "三角メッシュをエクスポート... (&M)" + +#: graphicswin.cpp:53 +msgid "Export &Surfaces..." +msgstr "サーフェスをエクスポート... (&S)" + +#: graphicswin.cpp:54 +msgid "Im&port..." +msgstr "インポート... (&I)" + +#: graphicswin.cpp:57 +msgid "E&xit" +msgstr "終了 (&E)" + +#: graphicswin.cpp:60 +msgid "&Edit" +msgstr "編集 (&E)" + +#: graphicswin.cpp:61 +msgid "&Undo" +msgstr "取り消し (&U)" + +#: graphicswin.cpp:62 +msgid "&Redo" +msgstr "やり直し (&R)" + +#: graphicswin.cpp:63 +msgid "Re&generate All" +msgstr "すべて再生成 (&g)" + +#: graphicswin.cpp:65 +msgid "Snap Selection to &Grid" +msgstr "選択したオブジェクトをグリッドに合わせる (&G)" + +#: graphicswin.cpp:66 +msgid "Rotate Imported &90°" +msgstr "リンクした部品を90度回転する (&9)" + +#: graphicswin.cpp:68 +msgid "Cu&t" +msgstr "切り取り (&t)" + +#: graphicswin.cpp:69 +msgid "&Copy" +msgstr "コピー (&C)" + +#: graphicswin.cpp:70 +msgid "&Paste" +msgstr "貼り付け (&P)" + +#: graphicswin.cpp:71 +msgid "Paste &Transformed..." +msgstr "複製しながら貼り付け... (&T)" + +#: graphicswin.cpp:72 +msgid "&Delete" +msgstr "削除 (&D)" + +#: graphicswin.cpp:74 +msgid "Select &Edge Chain" +msgstr "接続しているエッジを選択" + +#: graphicswin.cpp:75 +msgid "Select &All" +msgstr "すべてを選択 (&A)" + +#: graphicswin.cpp:76 +msgid "&Unselect All" +msgstr "選択を解除 (&U)" + +#: graphicswin.cpp:78 +msgid "&Line Styles..." +msgstr "線のスタイル... (&L)" + +#: graphicswin.cpp:79 +msgid "&View Projection..." +msgstr "カメラの設定... (&V)" + +#: graphicswin.cpp:81 +msgid "Con&figuration..." +msgstr "設定 (&f)" + +#: graphicswin.cpp:84 +msgid "&View" +msgstr "表示 (&V)" + +#: graphicswin.cpp:85 +msgid "Zoom &In" +msgstr "拡大 (&I)" + +#: graphicswin.cpp:86 +msgid "Zoom &Out" +msgstr "縮小 (&O)" + +#: graphicswin.cpp:87 +msgid "Zoom To &Fit" +msgstr "選択オブジェクトの最適表示" + +#: graphicswin.cpp:89 +msgid "Align View to &Workplane" +msgstr "作業平面に合わせる (&W)" + +#: graphicswin.cpp:90 +msgid "Nearest &Ortho View" +msgstr "近傍の正面図視点へ移動 (&O)" + +#: graphicswin.cpp:91 +msgid "Nearest &Isometric View" +msgstr "近傍の等角図視点へ移動 (&I)" + +#: graphicswin.cpp:92 +msgid "&Center View At Point" +msgstr "点を表示の中心にする" + +#: graphicswin.cpp:94 +msgid "Show Snap &Grid" +msgstr "グリッドを表示 (&G)" + +#: graphicswin.cpp:95 +msgid "Darken Inactive Solids" +msgstr "アクティブでないソリッドを暗くする" + +#: graphicswin.cpp:96 +msgid "Use &Perspective Projection" +msgstr "透視投影で表示 (&P)" + +#: graphicswin.cpp:97 +msgid "Show E&xploded View" +msgstr "分解立体図を表示 (&x)" + +#: graphicswin.cpp:98 +msgid "Dimension &Units" +msgstr "表示単位の選択 (&U)" + +#: graphicswin.cpp:99 +msgid "Dimensions in &Millimeters" +msgstr "ミリメートル (&M)" + +#: graphicswin.cpp:100 +msgid "Dimensions in M&eters" +msgstr "メートル (&e)" + +#: graphicswin.cpp:101 +msgid "Dimensions in &Inches" +msgstr "インチ (&I)" + +#: graphicswin.cpp:102 +msgid "Dimensions in &Feet and Inches" +msgstr "フィート・インチ (&F)" + +#: graphicswin.cpp:104 +msgid "Show &Toolbar" +msgstr "ツールバーを表示 (&T)" + +#: graphicswin.cpp:105 +msgid "Show Property Bro&wser" +msgstr "プロパティブラウザを表示 (&w)" + +#: graphicswin.cpp:107 +msgid "&Full Screen" +msgstr "全画面表示 (&F)" + +#: graphicswin.cpp:109 +msgid "&New Group" +msgstr "グループ (&N)" + +#: graphicswin.cpp:110 +msgid "Sketch In &3d" +msgstr "三次元空間内でスケッチを始める (&3)" + +#: graphicswin.cpp:111 +msgid "Sketch In New &Workplane" +msgstr "新しい作業平面上でスケッチを始める (&W)" + +#: graphicswin.cpp:113 +msgid "Step &Translating" +msgstr "直線上にコピー (&T)" + +#: graphicswin.cpp:114 +msgid "Step &Rotating" +msgstr "円上にコピー (&R)" + +#: graphicswin.cpp:116 +msgid "E&xtrude" +msgstr "押し出し (&x)" + +#: graphicswin.cpp:117 +msgid "&Helix" +msgstr "周回 (&H)" + +#: graphicswin.cpp:118 +msgid "&Lathe" +msgstr "螺旋 (&L)" + +#: graphicswin.cpp:119 +msgid "Re&volve" +msgstr "回転 (&v)" + +#: graphicswin.cpp:121 +msgid "Link / Assemble..." +msgstr "リンク・アセンブル..." + +#: graphicswin.cpp:122 +msgid "Link Recent" +msgstr "最近使用したファイルとリンク" + +#: graphicswin.cpp:124 +msgid "&Sketch" +msgstr "スケッチ (&S)" + +#: graphicswin.cpp:125 +msgid "In &Workplane" +msgstr "作業平面上でスケッチする (&W)" + +#: graphicswin.cpp:126 +msgid "Anywhere In &3d" +msgstr "三次元空間内でスケッチする (&3)" + +#: graphicswin.cpp:128 +msgid "Datum &Point" +msgstr "点 (&P)" + +#: graphicswin.cpp:129 +msgid "Wor&kplane" +msgstr "作業平面 (&k)" + +#: graphicswin.cpp:131 +msgid "Line &Segment" +msgstr "線分 (&S)" + +#: graphicswin.cpp:132 +msgid "C&onstruction Line Segment" +msgstr "補助線の線分 (&o)" + +#: graphicswin.cpp:133 +msgid "&Rectangle" +msgstr "四角形 (&R)" + +#: graphicswin.cpp:134 +msgid "&Circle" +msgstr "円 (&C)" + +#: graphicswin.cpp:135 +msgid "&Arc of a Circle" +msgstr "円弧 (&A)" + +#: graphicswin.cpp:136 +msgid "&Bezier Cubic Spline" +msgstr "ベジェ曲線 (&B)" + +#: graphicswin.cpp:138 +msgid "&Text in TrueType Font" +msgstr "TrueTypeフォントのテキスト (&T)" + +#: graphicswin.cpp:139 +msgid "I&mage" +msgstr "画像 (&m)" + +#: graphicswin.cpp:141 +msgid "To&ggle Construction" +msgstr "補助線との切り替え (&g)" + +#: graphicswin.cpp:142 +msgid "Ta&ngent Arc at Point" +msgstr "フィレット(&n)" + +#: graphicswin.cpp:143 +msgid "Split Curves at &Intersection" +msgstr "交差箇所で分割 (&I)" + +#: graphicswin.cpp:145 +msgid "&Constrain" +msgstr "拘束 (&C)" + +#: graphicswin.cpp:146 +msgid "&Distance / Diameter" +msgstr "距離・直径 (&D)" + +#: graphicswin.cpp:147 +msgid "Re&ference Dimension" +msgstr "参照寸法 (&f)" + +#: graphicswin.cpp:148 +msgid "A&ngle / Equal Angle" +msgstr "指定角度 / 角度を等しく (&n)" + +#: graphicswin.cpp:149 +msgid "Reference An&gle" +msgstr "参照角度 (&g)" + +#: graphicswin.cpp:150 +msgid "Other S&upplementary Angle" +msgstr "補角 (&u)" + +#: graphicswin.cpp:151 +msgid "Toggle R&eference Dim" +msgstr "拘束と参照を切り替え (&e)" + +#: graphicswin.cpp:153 +msgid "&Horizontal" +msgstr "æ°´å¹³ (&H)" + +#: graphicswin.cpp:154 +msgid "&Vertical" +msgstr "垂直 (&V)" + +#: graphicswin.cpp:156 +msgid "&On Point / Curve / Plane" +msgstr "一致 (&O)" + +#: graphicswin.cpp:157 +msgid "E&qual Length / Radius" +msgstr "長さ・半径を等値にする (&q)" + +#: graphicswin.cpp:158 +msgid "Length / Arc Ra&tio" +msgstr "長さ・円弧の比率 (&t)" + +#: graphicswin.cpp:159 +msgid "Length / Arc Diff&erence" +msgstr "長さ・円弧の差 (&e)" + +#: graphicswin.cpp:160 +msgid "At &Midpoint" +msgstr "中点 (&M)" + +#: graphicswin.cpp:161 +msgid "S&ymmetric" +msgstr "対称 (&y)" + +#: graphicswin.cpp:162 +msgid "Para&llel / Tangent" +msgstr "平行・正接 (&l)" + +#: graphicswin.cpp:163 +msgid "&Perpendicular" +msgstr "垂線 (&P)" + +#: graphicswin.cpp:164 +msgid "Same Orient&ation" +msgstr "同じ方向 (&a)" + +#: graphicswin.cpp:165 +msgid "Lock Point Where &Dragged" +msgstr "点を他のオブジェクトに連動して動かさない (&D)" + +#: graphicswin.cpp:167 +msgid "Comment" +msgstr "コメントを配置" + +#: graphicswin.cpp:169 +msgid "&Analyze" +msgstr "解析 (&A)" + +#: graphicswin.cpp:170 +msgid "Measure &Volume" +msgstr "体積を計測 (&V)" + +#: graphicswin.cpp:171 +msgid "Measure A&rea" +msgstr "面積を計測 (&r)" + +#: graphicswin.cpp:172 +msgid "Measure &Perimeter" +msgstr "外周長を計測 (&P)" + +#: graphicswin.cpp:173 +msgid "Show &Interfering Parts" +msgstr "部品の干渉を強調表示 (&I)" + +#: graphicswin.cpp:174 +msgid "Show &Naked Edges" +msgstr "浮いたエッジを強調表示 (&N)" + +#: graphicswin.cpp:175 +msgid "Show &Center of Mass" +msgstr "重心を表示 (&C)" + +#: graphicswin.cpp:177 +msgid "Show &Underconstrained Points" +msgstr "拘束されていない点を強調表示 (&U)" + +#: graphicswin.cpp:179 +msgid "&Trace Point" +msgstr "点の位置の記録を開始 (&T)" + +#: graphicswin.cpp:180 +msgid "&Stop Tracing..." +msgstr "記録を終了して保存する... (&S)" + +#: graphicswin.cpp:181 +msgid "Step &Dimension..." +msgstr "寸法を段階的に変更... (&D)" + +#: graphicswin.cpp:183 +msgid "&Help" +msgstr "ヘルプ (&H)" + +#: graphicswin.cpp:184 +msgid "&Language" +msgstr "言語 (&L)" + +#: graphicswin.cpp:185 +msgid "&Website / Manual" +msgstr "Webサイト・マニュアル" + +#: graphicswin.cpp:186 +msgid "&Go to GitHub commit" +msgstr "GitHubの該当コミットへ移動 (&G)" + +#: graphicswin.cpp:188 +msgid "&About" +msgstr "SolveSpaceについて (&A)" + +#: graphicswin.cpp:362 +msgid "(no recent files)" +msgstr "(履歴なし)" + +#: graphicswin.cpp:370 +#, c-format +msgid "File '%s' does not exist." +msgstr "ファイル '%s' は存在しません。" + +#: graphicswin.cpp:779 +msgid "No workplane is active, so the grid will not appear." +msgstr "作業平面がアクティブでないため、グリッドは表示されません。" + +#: graphicswin.cpp:794 +msgid "" +"The perspective factor is set to zero, so the view will always be a parallel " +"projection.\n" +"\n" +"For a perspective projection, modify the perspective factor in the " +"configuration screen. A value around 0.3 is typical." +msgstr "" +"透視投影の係数が 0 になっているため、表示は常に平行投影になります。\n" +"\n" +"透視投影にするには、係数を configuration から変更してください。値は 0.3 くら" +"いが一般的です。" + +#: graphicswin.cpp:884 +msgid "" +"Select a point; this point will become the center of the view on screen." +msgstr "スクリーンの中央とする点を指定してください。" + +#: graphicswin.cpp:1193 +msgid "No additional entities share endpoints with the selected entities." +msgstr "選択した要素と端点を共有する要素は、これ以上ありません。" + +#: graphicswin.cpp:1211 +msgid "" +"To use this command, select a point or other entity from an linked part, or " +"make a link group the active group." +msgstr "" +"このコマンドを利用するには、リンクされた部品の要素や点を選択するか、リンクの" +"グループをアクティブにしてください。" + +#: graphicswin.cpp:1234 +msgid "" +"No workplane is active. Activate a workplane (with Sketch -> In Workplane) " +"to define the plane for the snap grid." +msgstr "" +"作業平面がアクティブではありません。グリッドの平面を指定するには作業平面をア" +"クティブにしてください (「スケッチ -> 作業平面上でスケッチする」)。" + +#: graphicswin.cpp:1241 +msgid "" +"Can't snap these items to grid; select points, text comments, or constraints " +"with a label. To snap a line, select its endpoints." +msgstr "" +"この要素をグリッドに合わせることはできません。点、コメントテキスト、もしくは" +"値のある拘束を選択してください。直線をグリッドに合わせるには、端点を選択して" +"ください。" + +#: graphicswin.cpp:1326 +msgid "No workplane selected. Activating default workplane for this group." +msgstr "" +"作業平面がアクティブではありません。現在のグループのデフォルトの作業平面をア" +"クティブにします。" + +#: graphicswin.cpp:1329 +msgid "" +"No workplane is selected, and the active group does not have a default " +"workplane. Try selecting a workplane, or activating a sketch-in-new-" +"workplane group." +msgstr "" +"作業平面がアクティブではありません。また、現在のグループにはデフォルトの作業" +"平面がありません。作業平面を選択するか、新しく作業平面グループを作成してくだ" +"さい。" + +#: graphicswin.cpp:1350 +msgid "" +"Bad selection for tangent arc at point. Select a single point, or select " +"nothing to set up arc parameters." +msgstr "" +"フィェットの作成には無効な選択です。ひとつの点が選択されていればフィレットを" +"作成します。なにも選択されていなければ、フィレット作成時のパラメーターを設定" +"できます。" + +#: graphicswin.cpp:1361 +msgid "click point on arc (draws anti-clockwise)" +msgstr "円弧上の点をクリックで指定 (反時計回りに描きます)" + +#: graphicswin.cpp:1362 +msgid "click to place datum point" +msgstr "点の位置をクリックで指定" + +#: graphicswin.cpp:1363 +msgid "click first point of line segment" +msgstr "線分の最初の点をクリックで指定" + +#: graphicswin.cpp:1365 +msgid "click first point of construction line segment" +msgstr "補助線の線分の最初の点をクリックで指定" + +#: graphicswin.cpp:1366 +msgid "click first point of cubic segment" +msgstr "ベジェ曲線の最初の点をクリックで指定" + +#: graphicswin.cpp:1367 +msgid "click center of circle" +msgstr "円の中央をクリックで指定" + +#: graphicswin.cpp:1368 +msgid "click origin of workplane" +msgstr "作業平面の原点をクリックで指定" + +#: graphicswin.cpp:1369 +msgid "click one corner of rectangle" +msgstr "四角形の角のひとつをクリックで指定" + +#: graphicswin.cpp:1370 +msgid "click top left of text" +msgstr "テキストの右上をクリックで指定する" + +#: graphicswin.cpp:1376 +msgid "click top left of image" +msgstr "画像の右上をクリックで指定する" + +#: graphicswin.cpp:1402 +msgid "" +"No entities are selected. Select entities before trying to toggle their " +"construction state." +msgstr "" +"スケッチ要素が選択されていません。拘束と参照を切り替えるには、最初に要素を選" +"択してください。" + +#: group.cpp:86 +msgctxt "group-name" +msgid "sketch-in-3d" +msgstr "" + +#: group.cpp:154 +msgid "" +"Bad selection for new sketch in workplane. This group can be created with:\n" +"\n" +" * a point (through the point, orthogonal to coordinate axes)\n" +" * a point and two line segments (through the point, parallel to the " +"lines)\n" +" * a point and a normal (through the point, orthogonal to the normal)\n" +" * a workplane (copy of the workplane)\n" +msgstr "" +"作業平面にスケッチを作成できませんでした。次のように選択してください。\n" +"\n" +" * 点 (点を通り、座標軸に直交する作業平面を作成します)\n" +" * 点と、2 つの線分 (点を通り、直線に平行する作業平面を作成します)\n" +" * 点と、法線 (点を通り、法線と直交する作業平面を作成します)\n" +" * 作業平面 (あるいは作業平面のコピー)\n" + +#: group.cpp:170 +msgid "" +"Activate a workplane (Sketch -> In Workplane) before extruding. The sketch " +"will be extruded normal to the workplane." +msgstr "" +"押し出しを作成する前に、「スケッチ -> 作業平面上でスケッチする」で作業平面を" +"アクティブにしてください。作業平面の法線方向へ押し出しを作成します。" + +#: group.cpp:179 +msgctxt "group-name" +msgid "extrude" +msgstr "" + +#: group.cpp:184 +msgid "Lathe operation can only be applied to planar sketches." +msgstr "周回操作は作業平面上にあるスケッチにのみ適用できます。" + +#: group.cpp:195 +msgid "" +"Bad selection for new lathe group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"周回を作成できませんでした。次のように選択してください。\n" +"\n" +" * 点と、線分もしくは法線 (点を通る回転軸を仮定し、直線・法線に平行に回転し" +"ます)\n" +" * 線分 (直線を回転軸とします)\n" + +#: group.cpp:205 +msgctxt "group-name" +msgid "lathe" +msgstr "" + +#: group.cpp:210 +msgid "Revolve operation can only be applied to planar sketches." +msgstr "回転操作は作業平面上にあるスケッチにのみ適用できます。" + +#: group.cpp:221 +msgid "" +"Bad selection for new revolve group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"回転を作成できませんでした。次のように選択してください。\n" +"\n" +" * 点と、線分もしくは法線 (点を通る回転軸を仮定し、直線・法線に平行に回転し" +"ます)\n" +" * 線分 (直線を回転軸とします)\n" + +#: group.cpp:233 +msgctxt "group-name" +msgid "revolve" +msgstr "" + +#: group.cpp:238 +msgid "Helix operation can only be applied to planar sketches." +msgstr "螺旋操作は作業平面上にあるスケッチにのみ適用できます。" + +#: group.cpp:249 +msgid "" +"Bad selection for new helix group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"螺旋を作成できませんでした。次のように選択してください。\n" +"\n" +" * 点と、線分もしくは法線 (点を通る回転軸を仮定し、直線・法線に平行に螺旋を" +"作成します)\n" +" * 線分 (直線を回転軸とします)\n" + +#: group.cpp:261 +msgctxt "group-name" +msgid "helix" +msgstr "" + +#: group.cpp:274 +msgid "" +"Bad selection for new rotation. This group can be created with:\n" +"\n" +" * a point, while locked in workplane (rotate in plane, about that " +"point)\n" +" * a point and a line or a normal (rotate about an axis through the " +"point, and parallel to line / normal)\n" +msgstr "" +"円上にコピーを作成できませんでした。次のように選択してください。\n" +"\n" +" * 作業平面上にある点 (作業平面上で、これを原点として周回します)\n" +" * 点と、直線もしくは法線 (点を通る回転軸を仮定し、直線・法線と平行に周回し" +"ます)\n" + +#: group.cpp:287 +msgctxt "group-name" +msgid "rotate" +msgstr "" + +#: group.cpp:298 +msgctxt "group-name" +msgid "translate" +msgstr "" + +#: group.cpp:422 +msgid "(unnamed)" +msgstr "(名称未設定)" + +#: groupmesh.cpp:710 +msgid "not closed contour, or not all same style!" +msgstr "" +"閉じた輪郭でない、もしくは、輪郭を構成する要素のスタイルが同一ではありませ" +"ん。" + +#: groupmesh.cpp:723 +msgid "points not all coplanar!" +msgstr "点が同一平面上にありません。" + +#: groupmesh.cpp:725 +msgid "contour is self-intersecting!" +msgstr "輪郭線が自己交差しています。" + +#: groupmesh.cpp:727 +msgid "zero-length edge!" +msgstr "長さが 0 のエッジです。" + +#: importmesh.cpp:136 +msgid "Text-formated STL files are not currently supported" +msgstr "テキスト形式のSTLファイルは、いまのところサポートされていません。" + +#: modify.cpp:252 +msgid "Must be sketching in workplane to create tangent arc." +msgstr "正接円弧をスケッチするには、作業平面上でスケッチしてください。" + +#: modify.cpp:299 +msgid "" +"To create a tangent arc, select a point where two non-construction lines or " +"circles in this group and workplane join." +msgstr "" +"正接円弧を作成するには、同じグループかつ同じ作業平面上にある、補助線ではない2" +"つの直線もしくは円が交差している点を選択してください。" + +#: modify.cpp:386 +msgid "" +"Couldn't round this corner. Try a smaller radius, or try creating the " +"desired geometry by hand with tangency constraints." +msgstr "" +"この角をフィレットにできませんでした。もっと小さい半径を指定するか、手作業で" +"拘束してください。" + +#: modify.cpp:595 +msgid "Couldn't split this entity; lines, circles, or cubics only." +msgstr "この要素は分割できません。分割できるのは直線・円・ベジェ曲線のみです。" + +#: modify.cpp:622 +msgid "Must be sketching in workplane to split." +msgstr "分割するには作業平面上でスケッチしてください。" + +#: modify.cpp:629 +msgid "" +"Select two entities that intersect each other (e.g. two lines/circles/arcs " +"or a line/circle/arc and a point)." +msgstr "互いに交差している 2 つの要素を選択してください (直線・円・円弧・点)" + +#: modify.cpp:734 +msgid "Can't split; no intersection found." +msgstr "交差箇所が見つからないため、分割できません。" + +#: mouse.cpp:558 +msgid "Assign to Style" +msgstr "スタイルを割り当て" + +#: mouse.cpp:574 +msgid "No Style" +msgstr "スタイル指定なし" + +#: mouse.cpp:577 +msgid "Newly Created Custom Style..." +msgstr "スタイルを作成して割り当て..." + +#: mouse.cpp:584 +msgid "Group Info" +msgstr "グループ情報" + +#: mouse.cpp:604 +msgid "Style Info" +msgstr "スタイル情報" + +#: mouse.cpp:624 +msgid "Select Edge Chain" +msgstr "接続しているエッジをまとめて選択" + +#: mouse.cpp:630 +msgid "Toggle Reference Dimension" +msgstr "参照寸法と切り替え" + +#: mouse.cpp:636 +msgid "Other Supplementary Angle" +msgstr "補角と切り替え" + +#: mouse.cpp:641 +msgid "Snap to Grid" +msgstr "グリッドに沿わせる" + +#: mouse.cpp:650 +msgid "Remove Spline Point" +msgstr "スプラインの制御点を削除" + +#: mouse.cpp:685 +msgid "Add Spline Point" +msgstr "スプラインの制御点を追加" + +#: mouse.cpp:689 +msgid "Cannot add spline point: maximum number of points reached." +msgstr "" +"スプラインの制御点を追加できませんでした。点の個数の上限に達しています。" + +#: mouse.cpp:714 +msgid "Toggle Construction" +msgstr "補助線との切り替え" + +#: mouse.cpp:730 +msgid "Delete Point-Coincident Constraint" +msgstr "点の一致拘束を削除する" + +#: mouse.cpp:748 +msgid "Cut" +msgstr "切り取り" + +#: mouse.cpp:750 +msgid "Copy" +msgstr "コピー" + +#: mouse.cpp:754 +msgid "Select All" +msgstr "すべてを選択" + +#: mouse.cpp:759 +msgid "Paste" +msgstr "貼り付け" + +#: mouse.cpp:761 +msgid "Paste Transformed..." +msgstr "複製しながら貼り付け..." + +#: mouse.cpp:766 +msgid "Delete" +msgstr "削除" + +#: mouse.cpp:769 +msgid "Unselect All" +msgstr "選択を解除" + +#: mouse.cpp:776 +msgid "Unselect Hovered" +msgstr "カーソルを当てている要素の選択を解除" + +#: mouse.cpp:785 +msgid "Zoom to Fit" +msgstr "選択オブジェクトの最適表示" + +#: mouse.cpp:987 mouse.cpp:1276 +msgid "click next point of line, or press Esc" +msgstr "直線の次の点をクリックで指定、もしくはESCで終了" + +#: mouse.cpp:993 +msgid "" +"Can't draw rectangle in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"三次元空間内に四角形はスケッチできません。「スケッチ -> 作業平面上でスケッチ" +"する」で作業平面をアクティブにしてください。" + +#: mouse.cpp:1027 +msgid "click to place other corner of rectangle" +msgstr "四角形のもう一つの角をクリックで指定" + +#: mouse.cpp:1048 +msgid "click to set radius" +msgstr "半径をクリックで指定" + +#: mouse.cpp:1053 +msgid "" +"Can't draw arc in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"三次元空間内に円弧はスケッチできません。「スケッチ -> 作業平面上でスケッチす" +"る」で作業平面をアクティブにしてください。" + +#: mouse.cpp:1072 +msgid "click to place point" +msgstr "点の位置をクリックで指定" + +#: mouse.cpp:1088 +msgid "click next point of cubic, or press Esc" +msgstr "ベジェ曲線の次の点をクリックで指定、もしくはESCで終了" + +#: mouse.cpp:1093 +msgid "" +"Sketching in a workplane already; sketch in 3d before creating new workplane." +msgstr "" +"すでに作業平面上でのスケッチをしています。新しい作業平面を作成する前に、三次" +"元空間内でスケッチしてください。" + +#: mouse.cpp:1109 +msgid "" +"Can't draw text in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"三次元空間内にテキストはスケッチできません。「スケッチ -> 作業平面上でスケッ" +"チする」で作業平面をアクティブにしてください。" + +#: mouse.cpp:1126 +msgid "click to place bottom right of text" +msgstr "テキストの左下の位置をクリックで指定" + +#: mouse.cpp:1132 +msgid "" +"Can't draw image in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"三次元空間内に画像はスケッチできません。「スケッチ -> 作業平面上でスケッチす" +"る」で作業平面をアクティブにしてください。" + +#: platform/gui.cpp:85 platform/gui.cpp:90 solvespace.cpp:583 +msgctxt "file-type" +msgid "SolveSpace models" +msgstr "SolveSpaceモデル" + +#: platform/gui.cpp:89 +msgctxt "file-type" +msgid "ALL" +msgstr "(すべてのファイル)" + +#: platform/gui.cpp:91 +msgctxt "file-type" +msgid "IDF circuit board" +msgstr "IDF 電子基板" + +#: platform/gui.cpp:92 +msgctxt "file-type" +msgid "STL triangle mesh" +msgstr "STL 三角メッシュ" + +#: platform/gui.cpp:96 +msgctxt "file-type" +msgid "PNG image" +msgstr "PNG 画像" + +#: platform/gui.cpp:100 +msgctxt "file-type" +msgid "STL mesh" +msgstr "STL メッシュ" + +#: platform/gui.cpp:101 +msgctxt "file-type" +msgid "Wavefront OBJ mesh" +msgstr "Wavefront OBJ メッシュ" + +#: platform/gui.cpp:102 +msgctxt "file-type" +msgid "Three.js-compatible mesh, with viewer" +msgstr "Three.js 互換のメッシュ (ビューワー付)" + +#: platform/gui.cpp:103 +msgctxt "file-type" +msgid "Three.js-compatible mesh, mesh only" +msgstr "Three.js 互換のメッシュ (メッシュのみ)" + +#: platform/gui.cpp:104 +msgctxt "file-type" +msgid "VRML text file" +msgstr "VRML テキストファイル" + +#: platform/gui.cpp:108 platform/gui.cpp:115 platform/gui.cpp:122 +msgctxt "file-type" +msgid "STEP file" +msgstr "STEP ファイル" + +#: platform/gui.cpp:112 +msgctxt "file-type" +msgid "PDF file" +msgstr "PDF ファイル" + +#: platform/gui.cpp:113 +msgctxt "file-type" +msgid "Encapsulated PostScript" +msgstr "EPS ファイル (Encapsulated PostScript)" + +#: platform/gui.cpp:114 +msgctxt "file-type" +msgid "Scalable Vector Graphics" +msgstr "SVG ファイル" + +#: platform/gui.cpp:116 platform/gui.cpp:123 +msgctxt "file-type" +msgid "DXF file (AutoCAD 2007)" +msgstr "DXF ファイル (AutoDesk 2007)" + +#: platform/gui.cpp:117 +msgctxt "file-type" +msgid "HPGL file" +msgstr "HPG ファイル" + +#: platform/gui.cpp:118 +msgctxt "file-type" +msgid "G Code" +msgstr "G コード" + +#: platform/gui.cpp:127 +msgctxt "file-type" +msgid "AutoCAD DXF and DWG files" +msgstr "AudoCAD DXF/DWG ファイル" + +#: platform/gui.cpp:131 +msgctxt "file-type" +msgid "Comma-separated values" +msgstr "CSV ファイル (Comma-separated values)" + +#: platform/guigtk.cpp:1434 platform/guimac.mm:1513 platform/guiwin.cpp:1641 +msgid "untitled" +msgstr "名称未設定" + +#: platform/guigtk.cpp:1445 platform/guigtk.cpp:1481 platform/guimac.mm:1471 +#: platform/guiwin.cpp:1639 +msgctxt "title" +msgid "Save File" +msgstr "ファイルを保存" + +#: platform/guigtk.cpp:1446 platform/guigtk.cpp:1482 platform/guimac.mm:1454 +#: platform/guiwin.cpp:1645 +msgctxt "title" +msgid "Open File" +msgstr "ファイルを開く" + +#: platform/guigtk.cpp:1449 platform/guigtk.cpp:1488 +msgctxt "button" +msgid "_Cancel" +msgstr "キャンセル (_C)" + +#: platform/guigtk.cpp:1450 platform/guigtk.cpp:1486 +msgctxt "button" +msgid "_Save" +msgstr "保存 (_S)" + +#: platform/guigtk.cpp:1451 platform/guigtk.cpp:1487 +msgctxt "button" +msgid "_Open" +msgstr "開く (_O)" + +#: solvespace.cpp:175 +msgctxt "title" +msgid "Autosave Available" +msgstr "オートセーブファイルがあります" + +#: solvespace.cpp:176 +msgctxt "dialog" +msgid "An autosave file is available for this sketch." +msgstr "このスケッチにはオートセーブファイルがあります。" + +#: solvespace.cpp:177 +msgctxt "dialog" +msgid "Do you want to load the autosave file instead?" +msgstr "代わりにオートセーブファイル読み込みますか?" + +#: solvespace.cpp:178 +msgctxt "button" +msgid "&Load autosave" +msgstr "オートセーブファイルを読み込む (&L)" + +#: solvespace.cpp:180 +msgctxt "button" +msgid "Do&n't Load" +msgstr "読み込まない (&n)" + +#: solvespace.cpp:640 +msgctxt "title" +msgid "Modified File" +msgstr "編集済みファイル" + +#: solvespace.cpp:642 +#, c-format +msgctxt "dialog" +msgid "Do you want to save the changes you made to the sketch “%s”?" +msgstr "スケッチ \"%s\" への変更を保存しますか?" + +#: solvespace.cpp:645 +msgctxt "dialog" +msgid "Do you want to save the changes you made to the new sketch?" +msgstr "新規スケッチへの変更を保存しますか?" + +#: solvespace.cpp:648 +msgctxt "dialog" +msgid "Your changes will be lost if you don't save them." +msgstr "(未保存の変更があります。)" + +#: solvespace.cpp:649 +msgctxt "button" +msgid "&Save" +msgstr "保存する (&S)" + +#: solvespace.cpp:651 +msgctxt "button" +msgid "Do&n't Save" +msgstr "保存しない (&n)" + +#: solvespace.cpp:672 +msgctxt "title" +msgid "(new sketch)" +msgstr "(新規スケッチ)" + +#: solvespace.cpp:683 +msgctxt "title" +msgid "Property Browser" +msgstr "プロパティブラウザ" + +#: solvespace.cpp:746 +msgid "" +"Constraints are currently shown, and will be exported in the toolpath. This " +"is probably not what you want; hide them by clicking the link at the top of " +"the text window." +msgstr "" +"表示されている拘束はツールパスとしてエクスポートされます。これはあなたが期待" +"した動作ではないかもしれません。プロパティブラウザの上部にあるボタンで非表示" +"にできます。" + +#: solvespace.cpp:834 +#, c-format +msgid "" +"Can't identify file type from file extension of filename '%s'; try .dxf or ." +"dwg." +msgstr "" +"拡張子からファイル形式を特定できませんでした ( '%s' )。 \".dxf\" や \".dwg\" " +"を試してください。" + +#: solvespace.cpp:886 +msgid "Constraint must have a label, and must not be a reference dimension." +msgstr "参照寸法ではなく、かつ、値のある拘束を選択してください。" + +#: solvespace.cpp:890 +msgid "Bad selection for step dimension; select a constraint." +msgstr "寸法の段階的変更には無効な選択です。値のある拘束を選択してください。" + +#: solvespace.cpp:914 +msgid "The assembly does not interfere, good." +msgstr "部品の干渉はありませんでした。" + +#: solvespace.cpp:930 +#, c-format +msgid "" +"The volume of the solid model is:\n" +"\n" +" %s" +msgstr "" +"ソリッドモデルの容積:\n" +"\n" +" %s" + +#: solvespace.cpp:939 +#, c-format +msgid "" +"\n" +"The volume of current group mesh is:\n" +"\n" +" %s" +msgstr "" +"\n" +"現在のグループのメッシュの容積:\n" +"\n" +" %s" + +#: solvespace.cpp:944 +msgid "" +"\n" +"\n" +"Curved surfaces have been approximated as triangles.\n" +"This introduces error, typically of around 1%." +msgstr "" +"\n" +"\n" +"曲面はトライアングルに近似します。\n" +"これにより通常、約 1% の誤差が発生します。" + +#: solvespace.cpp:959 +#, c-format +msgid "" +"The surface area of the selected faces is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"選択した面のファーフェスの面積:\n" +"\n" +" %s\n" +"\n" +"曲線は区分線形に近似します。\n" +"これにより通常、約 1%% の誤差が発生します。" + +#: solvespace.cpp:968 +msgid "" +"This group does not contain a correctly-formed 2d closed area. It is open, " +"not coplanar, or self-intersecting." +msgstr "" +"このグループは正常に構成された二次元の閉じた領域が含まれていません。領域が開" +"いているか、同一平面上にないか、自己交差しています。" + +#: solvespace.cpp:980 +#, c-format +msgid "" +"The area of the region sketched in this group is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"このグループ内のスケッチされた領域の面積:\n" +"\n" +" %s\n" +"\n" +"曲線は区分線形に近似します。\n" +"これにより通常、約 1%% の誤差が発生します。" + +#: solvespace.cpp:1000 +#, c-format +msgid "" +"The total length of the selected entities is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"選択された要素の合計の長さ:\n" +"\n" +" %s\n" +"\n" +"曲線は区分線形に近似します。\n" +"これにより通常、約 1%% の誤差が発生します。" + +#: solvespace.cpp:1006 +msgid "Bad selection for perimeter; select line segments, arcs, and curves." +msgstr "" +"外周長の計測には無効な選択です。線分、円弧、もしくは曲線を選択してください。" + +#: solvespace.cpp:1022 +msgid "Bad selection for trace; select a single point." +msgstr "点の位置の記録には無効な選択です。ひとつの点を選択してください。" + +#: solvespace.cpp:1049 +#, c-format +msgid "Couldn't write to '%s'" +msgstr "'%s' へ書き込めませんでした。" + +#: solvespace.cpp:1079 +msgid "The mesh is self-intersecting (NOT okay, invalid)." +msgstr "メッシュが自身と交差しています (警告)" + +#: solvespace.cpp:1080 +msgid "The mesh is not self-intersecting (okay, valid)." +msgstr "メッシュは自己交差していません (適切)" + +#: solvespace.cpp:1082 +msgid "The mesh has naked edges (NOT okay, invalid)." +msgstr "メッシュには浮いたエッジが含まれています (警告)" + +#: solvespace.cpp:1083 +msgid "The mesh is watertight (okay, valid)." +msgstr "メッシュには隙間はありません (適切)" + +#: solvespace.cpp:1086 +#, c-format +msgid "" +"\n" +"\n" +"The model contains %d triangles, from %d surfaces." +msgstr "" +"\n" +"\n" +"モデルには %d 個のトライアングルがあり、これが %d 個のサーフェスを構成しま" +"す。" + +#: solvespace.cpp:1090 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"Zero problematic edges, good.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"問題のあるエッジは見つかりませんでした。%s" + +#: solvespace.cpp:1093 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d problematic edges, bad.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d 個の問題のあるエッジが見つかりました。%s" + +#: solvespace.cpp:1106 +#, c-format +msgid "" +"This is SolveSpace version %s.\n" +"\n" +"For more information, see http://solvespace.com/\n" +"\n" +"SolveSpace is free software: you are free to modify\n" +"and/or redistribute it under the terms of the GNU\n" +"General Public License (GPL) version 3 or later.\n" +"\n" +"There is NO WARRANTY, to the extent permitted by\n" +"law. For details, visit http://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues and other authors.\n" +msgstr "" + +#: style.cpp:185 +msgid "" +"Can't assign style to an entity that's derived from another entity; try " +"assigning a style to this entity's parent." +msgstr "" +"他の要素から派生した要素には個別のスタイルを割り当てることはできません。親要" +"素にスタイルを割り当ててください。" + +#: style.cpp:735 +msgid "Style name cannot be empty" +msgstr "スタイル名は空にできません" + +#: textscreens.cpp:837 +msgid "Can't repeat fewer than 1 time." +msgstr "繰り返しは 1 以上の回数を指定してください。" + +#: textscreens.cpp:841 +msgid "Can't repeat more than 999 times." +msgstr "999回を超えて繰り返すことはできません。" + +#: textscreens.cpp:866 +msgid "Group name cannot be empty" +msgstr "グループ名は空にできません" + +#: textscreens.cpp:918 +msgid "Opacity must be between zero and one." +msgstr "透明度は0から1の間で指定してください。" + +#: textscreens.cpp:953 +msgid "Radius cannot be zero or negative." +msgstr "半径は0もしくは負の値にできません。" + +#: toolbar.cpp:18 +msgid "Sketch line segment" +msgstr "線分をスケッチ" + +#: toolbar.cpp:20 +msgid "Sketch rectangle" +msgstr "四角形をスケッチ" + +#: toolbar.cpp:22 +msgid "Sketch circle" +msgstr "円をスケッチ" + +#: toolbar.cpp:24 +msgid "Sketch arc of a circle" +msgstr "円弧をスケッチ" + +#: toolbar.cpp:26 +msgid "Sketch curves from text in a TrueType font" +msgstr "TrueTypeフォントのテキストからアウトラインをスケッチ" + +#: toolbar.cpp:28 +msgid "Sketch image from a file" +msgstr "ファイルから画像を取り込む" + +#: toolbar.cpp:30 +msgid "Create tangent arc at selected point" +msgstr "選択された点でフィレットを作る" + +#: toolbar.cpp:32 +msgid "Sketch cubic Bezier spline" +msgstr "ベジェ曲線をスケッチ" + +#: toolbar.cpp:34 +msgid "Sketch datum point" +msgstr "点をスケッチ" + +#: toolbar.cpp:36 +msgid "Toggle construction" +msgstr "補助線と切り替え" + +#: toolbar.cpp:38 +msgid "Split lines / curves where they intersect" +msgstr "直線・曲線を交差箇所で分割" + +#: toolbar.cpp:42 +msgid "Constrain distance / diameter / length" +msgstr "距離・直径・長さを拘束" + +#: toolbar.cpp:44 +msgid "Constrain angle" +msgstr "角度を拘束" + +#: toolbar.cpp:46 +msgid "Constrain to be horizontal" +msgstr "水平に拘束" + +#: toolbar.cpp:48 +msgid "Constrain to be vertical" +msgstr "垂直に拘束" + +#: toolbar.cpp:50 +msgid "Constrain to be parallel or tangent" +msgstr "平行もしくは正接で拘束" + +#: toolbar.cpp:52 +msgid "Constrain to be perpendicular" +msgstr "垂線で拘束" + +#: toolbar.cpp:54 +msgid "Constrain point on line / curve / plane / point" +msgstr "点を線・曲線・面・点の上に拘束" + +#: toolbar.cpp:56 +msgid "Constrain symmetric" +msgstr "対称に拘束" + +#: toolbar.cpp:58 +msgid "Constrain equal length / radius / angle" +msgstr "長さ・半径・角度を等値に拘束" + +#: toolbar.cpp:60 +msgid "Constrain normals in same orientation" +msgstr "法線を同じ方向に拘束" + +#: toolbar.cpp:62 +msgid "Other supplementary angle" +msgstr "角度の拘束を補角へ移動" + +#: toolbar.cpp:64 +msgid "Toggle reference dimension" +msgstr "拘束と参照寸法を切り替え" + +#: toolbar.cpp:68 +msgid "New group extruding active sketch" +msgstr "押し出しを作成" + +#: toolbar.cpp:70 +msgid "New group rotating active sketch" +msgstr "周回を作成" + +#: toolbar.cpp:72 +msgid "New group helix from active sketch" +msgstr "螺旋を作成" + +#: toolbar.cpp:74 +msgid "New group revolve active sketch" +msgstr "回転を作成" + +#: toolbar.cpp:76 +msgid "New group step and repeat rotating" +msgstr "円上にコピーを作成" + +#: toolbar.cpp:78 +msgid "New group step and repeat translating" +msgstr "直線上にコピーを作成" + +#: toolbar.cpp:80 +msgid "New group in new workplane (thru given entities)" +msgstr "選択した要素で定義される作業平面でスケッチを開始" + +#: toolbar.cpp:82 +msgid "New group in 3d" +msgstr "三次元空間内でスケッチを開始" + +#: toolbar.cpp:84 +msgid "New group linking / assembling file" +msgstr "他のファイルをリンクする" + +#: toolbar.cpp:88 +msgid "Nearest isometric view" +msgstr "近傍の等角図視点" + +#: toolbar.cpp:90 +msgid "Align view to active workplane" +msgstr "アクティブな作業平面に視点を合わせる" + +#: util.cpp:165 +msgctxt "title" +msgid "Error" +msgstr "エラー" + +#: util.cpp:165 +msgctxt "title" +msgid "Message" +msgstr "メッセージ" + +#: util.cpp:170 +msgctxt "button" +msgid "&OK" +msgstr "OK (&O)" + +#: view.cpp:127 +msgid "Scale cannot be zero or negative." +msgstr "縮尺は 0 や負の値にはできません。" + +#: view.cpp:139 view.cpp:148 +msgid "Bad format: specify x, y, z" +msgstr "書式が誤っています。\"x, y, z\" で指定してください。" + +#~ msgid "" +#~ "Bad selection for on point / curve / plane constraint. This constraint " +#~ "can apply to:\n" +#~ "\n" +#~ " * two points (points coincident)\n" +#~ " * a point and a workplane (point in plane)\n" +#~ " * a point and a line segment (point on line)\n" +#~ " * a point and a circle or arc (point on curve)\n" +#~ " * a point and a plane face (point on face)\n" +#~ msgstr "" +#~ "一致拘束ができませんでした。次の要素に適用できます:\n" +#~ "\n" +#~ " * 2 つの点 (点を同一位置に)\n" +#~ " * 1 つの点と 1 つの作業平面 (点を平面上に)\n" +#~ " * 1 つの点と 1 つの線分 (点を直線状に)\n" +#~ " * 1 つの点と、1つの円もしくは円弧 (点を曲線上に)\n" +#~ " * 1 つの点と 1 つの平面フェイス (点をフェイス上に)\n" + +#~ msgid "" +#~ "Bad selection for equal length / radius constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two line segments (equal length)\n" +#~ " * two line segments and two points (equal point-line distances)\n" +#~ " * a line segment and two points (equal point-line distances)\n" +#~ " * a line segment, and a point and line segment (point-line distance " +#~ "equals length)\n" +#~ " * four line segments or normals (equal angle between A,B and C,D)\n" +#~ " * three line segments or normals (equal angle between A,B and B,C)\n" +#~ " * two circles or arcs (equal radius)\n" +#~ " * a line segment and an arc (line segment length equals arc length)\n" +#~ msgstr "" +#~ "長さ・半径・角度の同値拘束ができませんでした。次の要素に適用できます:\n" +#~ "\n" +#~ " * 2 つの線分 (長さを同じに)\n" +#~ " * 2 つの線分と 2 つの点 (それぞれ直線と点の距離を同じに)\n" +#~ " * 1 つの線分と 2 つの点 (直線とそれぞれの点の距離を同じに)\n" +#~ " * 1 つの線分と、1 つの点と 1 つの線分 (線分の長さと、直線と点の距離を同" +#~ "じに)\n" +#~ " * 4つの線分もしくは法線 (直線A,B、直線C,Dそれぞれの交差する角度を同じ" +#~ "に)\n" +#~ " * 3 つの線分もしくは法線 (直線A,B、直線B,Cそれぞれの交差する角度を同じ" +#~ "に)\n" +#~ " * 2 つの円もしくは円弧 (半径を同じに)\n" +#~ " * 1 つの線分と 1 つの円弧 (線分の長さと円弧の長さを同じに)\n" + +#~ msgid "" +#~ "Bad selection for horizontal / vertical constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two points\n" +#~ " * a line segment\n" +#~ msgstr "" +#~ "垂直・水平拘束ができませんでした。次の要素に適用できます:\n" +#~ "\n" +#~ " * 2 つの点\n" +#~ " * 1 つの線分\n" + +#~ msgid "" +#~ "Bad selection for angle constraint. This constraint can apply to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ " * a line segment and a normal\n" +#~ " * two normals\n" +#~ msgstr "" +#~ "角度拘束ができませんでした。次の要素に適用できます:\n" +#~ "\n" +#~ " * 2 つの線分\n" +#~ " * 線分と法線\n" +#~ " * 2 つの法線\n" + +#~ msgid "" +#~ "Bad selection for parallel / tangent constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two line segments (parallel)\n" +#~ " * a line segment and a normal (parallel)\n" +#~ " * two normals (parallel)\n" +#~ " * two line segments, arcs, or beziers, that share an endpoint " +#~ "(tangent)\n" +#~ msgstr "" +#~ "平行・正接拘束ができませんでした。次の要素に適用できます:\n" +#~ "\n" +#~ " * 2 つの線分 (平行)\n" +#~ " * 1 つの線分と 1 つの法線 (平行)\n" +#~ " * 2 つの法線 (平行)\n" +#~ " * 片方の端点を共有する 2 つの線分、円弧、もしくはベジェ曲線 (正接)\n" + +#~ msgid "" +#~ "Bad selection for perpendicular constraint. This constraint can apply " +#~ "to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ " * a line segment and a normal\n" +#~ " * two normals\n" +#~ msgstr "" +#~ "垂線拘束ができませんでした。次の要素に適用できます:\n" +#~ "\n" +#~ " * 2 つの線分\n" +#~ " * 1 つの線分と 1 つの法線\n" +#~ " * 2 つの法線\n" + +#~ msgid "A&ngle" +#~ msgstr "角度 (&n)" + +#~ msgid "E&qual Length / Radius / Angle" +#~ msgstr "長さ・半径・角度を等値にする (&q)" diff --git a/res/locales/ru_RU.po b/res/locales/ru_RU.po new file mode 100644 index 0000000..36f1d2e --- /dev/null +++ b/res/locales/ru_RU.po @@ -0,0 +1,2642 @@ +# Russian translations for SolveSpace package. +# Copyright (C) 2017 the SolveSpace authors +# This file is distributed under the same license as the SolveSpace package. +# EvilSpirit , 2017. +# Olesya Gerasimenko , 2021. +# Alexandre Prokoudine , 2023. +# Milan Djurovic , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: SolveSpace 3.0\n" +"Report-Msgid-Bugs-To: phkahler@gmail.com\n" +"POT-Creation-Date: 2025-01-26 21:04+0200\n" +"PO-Revision-Date: 2025-02-22 14:48-0800\n" +"Last-Translator: Milan Djurovic \n" +"Language-Team: Russian \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" +"X-Generator: Gtranslator 41.0\n" + +#: clipboard.cpp:314 +msgid "" +"Cut, paste, and copy work only in a workplane.\n" +"\n" +"Activate one with Sketch -> In Workplane." +msgstr "" +"Копировать, вставить или вырезать можно\n" +"только находясь в рабочей плоскости.\n" +"Активируйте ее через Эскиз->В рабочей плоскости." + +#: clipboard.cpp:331 +msgid "Clipboard is empty; nothing to paste." +msgstr "Буфер обмена пуст; нечего вставлять." + +#: clipboard.cpp:378 +msgid "Number of copies to paste must be at least one." +msgstr "Укажите в поле 'количество' хотя бы одну копию для вставки." + +#: clipboard.cpp:394 textscreens.cpp:879 +msgid "Scale cannot be zero." +msgstr "Масштабный коэффициент не может быть нулевым." + +#: clipboard.cpp:436 +msgid "Select one point to define origin of rotation." +msgstr "Выберите одну точку в качестве центра вращения." + +#: clipboard.cpp:448 +msgid "Select two points to define translation vector." +msgstr "Выберите две точки, чтобы задать вектор смещения." + +#: clipboard.cpp:458 +msgid "" +"Transformation is identity. So all copies will be exactly on top of each " +"other." +msgstr "" +"Трансформация не задана. Все копии будут расположены в одном и том же месте." + +#: clipboard.cpp:462 +msgid "Too many items to paste; split this into smaller pastes." +msgstr "Слишком много элементов для вставки; разбейте на несколько частей." + +#: clipboard.cpp:467 +msgid "No workplane active." +msgstr "Нет активной рабочей плоскости." + +#: confscreen.cpp:410 +msgid "Bad format: specify coordinates as x, y, z" +msgstr "Неверный формат: введите координаты как x, y, z" + +#: confscreen.cpp:420 style.cpp:729 textscreens.cpp:910 +msgid "Bad format: specify color as r, g, b" +msgstr "Неверный формат: введите цвет как r, g, b" + +#: confscreen.cpp:446 +msgid "" +"The perspective factor will have no effect until you enable View -> Use " +"Perspective Projection." +msgstr "" +"Коэффициент перспективы не будет иметь эффект, пока вы не включите Вид-" +">Перспективная Проекция." + +#: confscreen.cpp:464 confscreen.cpp:474 +#, c-format +msgid "Specify between 0 and %d digits after the decimal." +msgstr "Введите число от 0 до %d." + +#: confscreen.cpp:486 +msgid "Export scale must not be zero!" +msgstr "Масштабный коэффициент не может быть нулевым!" + +#: confscreen.cpp:498 +msgid "Cutter radius offset must not be negative!" +msgstr "Радиус режущего инструмента не может быть отрицательным!" + +#: confscreen.cpp:557 +msgid "Bad value: autosave interval should be positive" +msgstr "" +"Неверное значение: интервал автосохранения должен быть положительным числом" + +#: confscreen.cpp:560 +msgid "Bad format: specify interval in integral minutes" +msgstr "" +"Неверный формат: введите целое число, чтобы задать интервал автосохранения" + +#: constraint.cpp:12 +msgctxt "constr-name" +msgid "pts-coincident" +msgstr "тчк-тчк-совпадение" + +#: constraint.cpp:13 +msgctxt "constr-name" +msgid "pt-pt-distance" +msgstr "тчк-тчк-расстояние" + +#: constraint.cpp:14 +msgctxt "constr-name" +msgid "pt-line-distance" +msgstr "тчк-линия-расстояние" + +#: constraint.cpp:15 +msgctxt "constr-name" +msgid "pt-plane-distance" +msgstr "тчк-плоск-расстояние" + +#: constraint.cpp:16 +msgctxt "constr-name" +msgid "pt-face-distance" +msgstr "тчк-грань-расстояние" + +#: constraint.cpp:17 +msgctxt "constr-name" +msgid "proj-pt-pt-distance" +msgstr "проекц-тчк-тчк-расст" + +#: constraint.cpp:18 +msgctxt "constr-name" +msgid "pt-in-plane" +msgstr "тчк-на-плоск" + +#: constraint.cpp:19 +msgctxt "constr-name" +msgid "pt-on-line" +msgstr "тчк-на-линии" + +#: constraint.cpp:20 +msgctxt "constr-name" +msgid "pt-on-face" +msgstr "тчк-на-грани" + +#: constraint.cpp:21 +msgctxt "constr-name" +msgid "eq-length" +msgstr "равенство-длин" + +#: constraint.cpp:22 +msgctxt "constr-name" +msgid "eq-length-and-pt-ln-dist" +msgstr "равен-длины-и-тчк-лин-расст" + +#: constraint.cpp:23 +msgctxt "constr-name" +msgid "eq-pt-line-distances" +msgstr "равен-тчк-линия-расстояний" + +#: constraint.cpp:24 +msgctxt "constr-name" +msgid "length-ratio" +msgstr "отношение-длин" + +#: constraint.cpp:25 +msgctxt "constr-name" +msgid "arc-arc-length-ratio" +msgstr "отношение-длин-дуга-дуга" + +#: constraint.cpp:26 +msgctxt "constr-name" +msgid "arc-line-length-ratio" +msgstr "отношение-длин-дуга-отрезок" + +#: constraint.cpp:27 +msgctxt "constr-name" +msgid "length-difference" +msgstr "разность-длин" + +#: constraint.cpp:28 +msgctxt "constr-name" +msgid "arc-arc-len-difference" +msgstr "разность-длин-дуга-дуга" + +#: constraint.cpp:29 +msgctxt "constr-name" +msgid "arc-line-len-difference" +msgstr "разность-длин-дуга-отрезок" + +#: constraint.cpp:30 +msgctxt "constr-name" +msgid "symmetric" +msgstr "симметричность" + +#: constraint.cpp:31 +msgctxt "constr-name" +msgid "symmetric-h" +msgstr "симметричность-гориз" + +#: constraint.cpp:32 +msgctxt "constr-name" +msgid "symmetric-v" +msgstr "симметричность-верт" + +#: constraint.cpp:33 +msgctxt "constr-name" +msgid "symmetric-line" +msgstr "симметричность-по-оси" + +#: constraint.cpp:34 +msgctxt "constr-name" +msgid "at-midpoint" +msgstr "на-середине" + +#: constraint.cpp:35 +msgctxt "constr-name" +msgid "horizontal" +msgstr "горизонтальность" + +#: constraint.cpp:36 +msgctxt "constr-name" +msgid "vertical" +msgstr "вертикальность" + +#: constraint.cpp:37 +msgctxt "constr-name" +msgid "diameter" +msgstr "диаметр" + +#: constraint.cpp:38 +msgctxt "constr-name" +msgid "pt-on-circle" +msgstr "тчк-на-окружности" + +#: constraint.cpp:39 +msgctxt "constr-name" +msgid "same-orientation" +msgstr "идентичная-ориентация" + +#: constraint.cpp:40 +msgctxt "constr-name" +msgid "angle" +msgstr "угол" + +#: constraint.cpp:41 +msgctxt "constr-name" +msgid "parallel" +msgstr "параллельность" + +#: constraint.cpp:42 +msgctxt "constr-name" +msgid "arc-line-tangent" +msgstr "кас-дуга-линия" + +#: constraint.cpp:43 +msgctxt "constr-name" +msgid "cubic-line-tangent" +msgstr "кас-сплайн-линия" + +#: constraint.cpp:44 +msgctxt "constr-name" +msgid "curve-curve-tangent" +msgstr "кас-кривых" + +#: constraint.cpp:45 +msgctxt "constr-name" +msgid "perpendicular" +msgstr "перпендикулярность" + +#: constraint.cpp:46 +msgctxt "constr-name" +msgid "eq-radius" +msgstr "равенство-радиусов" + +#: constraint.cpp:47 +msgctxt "constr-name" +msgid "eq-angle" +msgstr "равенство-углов" + +#: constraint.cpp:48 +msgctxt "constr-name" +msgid "eq-line-len-arc-len" +msgstr "равен-длины-линии-длины-дуги" + +#: constraint.cpp:49 +msgctxt "constr-name" +msgid "lock-where-dragged" +msgstr "фиксация" + +#: constraint.cpp:50 +msgctxt "constr-name" +msgid "comment" +msgstr "комментарий" + +#: constraint.cpp:151 +msgid "" +"The point you selected does not belong to the arc. The arc and line segment " +"do not share an end point.\n" +"\n" +"Select the end point of the arc at which you want it to be tangent to the " +"line." +msgstr "" +"Выбранная точка не принадлежит дуге. Дуга и отрезок не имеют общей конечной " +"точки.\n" +"\n" +"Выберите конечную точку дуги, в которой вы хотите, чтобы она была " +"касательной к отрезку." + +#: constraint.cpp:158 +msgid "" +"The tangent arc and line segment must share an endpoint. Constrain them with " +"Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the arc at which you want it to be " +"tangent to the line." +msgstr "" +"Дуга и отрезок должны быть соединены. Соедините их крайние точки с помощью " +"'Ограничения -> Точка на примитиве' перед тем, как применять ограничения " +"касательности.\n" +"\n" +"Или выберите конечную точку дуги, в которой вы хотите, чтобы она была " +"касательной к отрезку." + +#: constraint.cpp:186 +msgid "" +"The point you selected is not an end point of the cubic spline. The spline " +"and line segment do not share an end point.\n" +"\n" +"Select the end point of the spline at which you want it to be tangent to the " +"line." +msgstr "" +"Выбранная вами точка не является конечной точкой кубического сплайна. Сплайн " +"и отрезок не имеют общей конечной точки.\n" +"\n" +"Выберите конечную точку сплайна, в которой вы хотите, чтобы он был " +"касательным к отрезку." + +#: constraint.cpp:193 +msgid "" +"The tangent cubic spline and line segment must share an endpoint. Constrain " +"them with Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the cubic spline at which you want it " +"to be tangent to the line." +msgstr "" +"Сплайн и отрезок должны быть соединены. Соедините их крайние точки с помощью " +"'Ограничения -> Точка на Примитиве' перед тем, как применять ограничение " +"касательности.\n" +"\n" +"Или выберите конечную точку кубического сплайна, в которой вы хотите, чтобы " +"он былкасательным к отрезку." + +#: constraint.cpp:237 +msgid "" +"The points you selected are not end points of the two curves. The curves do " +"not share an end point.\n" +"\n" +"Select the end points of both curves at which you want them to be tangent to " +"each other." +msgstr "" +"Выбранные вами точки не являются конечными точками двух кривых. У кривых нет " +"общей конечной точки.\n" +"\n" +"Выберите конечные точки обеих кривых, в которых вы хотите, чтобы они были " +"касательными друг к другу." + +#: constraint.cpp:244 +msgid "" +"The curves must share an endpoint. Constrain them with Constrain -> On Point " +"before constraining tangent.\n" +"\n" +"Alternatively select the end points of both curves at which you want the " +"curves to be tangent." +msgstr "" +"Кривые должны быть соединены. Соедините их крайние точки с помощью " +"'Ограничения -> Точка на Примитиве' перед тем, как применять ограничение " +"касательности.\n" +"\n" +"Или выберите конечные точки обеих кривых, в которых вы хотите, чтобы они " +"быликасательными." + +#: constraint.cpp:303 +msgid "" +"Bad selection for distance / diameter constraint. This constraint can apply " +"to:\n" +"\n" +" * two points (distance between points)\n" +" * a line segment (length)\n" +" * two points and a line segment or normal (projected distance)\n" +" * a workplane and a point (minimum distance)\n" +" * a line segment and a point (minimum distance)\n" +" * a plane face and a point (minimum distance)\n" +" * a circle or an arc (diameter)\n" +msgstr "" +"Неправильное выделение для ограничения расстояния / диаметра.\n" +"Ограничение может принимать в качестве выделения следующие примитивы:\n" +"\n" +" * две точки (расстояние между точками)\n" +" * отрезок (длина отрезка)\n" +" * две точки и отрезок / нормаль (расстояние между точками, " +"спроецированное на линию / нормаль)\n" +" * рабочую плоскость и точку (расстояние от точки до плоскости)\n" +" * отрезок и точку (расстояние от точки до линии)\n" +" * грань и точку (расстояние от точки до плоскости грани)\n" +" * окружность или дугу (диаметр / радиус)\n" + +#: constraint.cpp:366 +msgid "" +"Bad selection for on point / curve / plane constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points (points coincident)\n" +" * a point and a workplane (point in plane)\n" +" * a point and a line segment (point on line)\n" +" * a point and a circle or arc (point on curve)\n" +" * a point and one to three plane faces (point on face(s))\n" +msgstr "" +"Неправильное выделение для ограничения 'точка на примитиве'.\n" +"Ограничение может принимать в качестве выделения следующие примитивы:\n" +"\n" +" * две или более точек (совпадение точек)\n" +" * точку и рабочую плоскость (точка в плоскости)\n" +" * точку и отрезок (точка на линии)\n" +" * точку и окружность / дугу / сплайн (точка на кривой)\n" +" * точку и от одной до трех граней (точка на грани)\n" + +#: constraint.cpp:427 +msgid "" +"Bad selection for equal length / radius constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more line segments (equal length)\n" +" * two line segments and two points (equal point-line distances)\n" +" * a line segment and two points (equal point-line distances)\n" +" * a line segment, and a point and line segment (point-line distance " +"equals length)\n" +" * two or more circles or arcs (equal radius)\n" +" * a line segment and an arc (line segment length equals arc length)\n" +msgstr "" +"Неправильное выделение для ограничения 'равенство примитивов'.\n" +"Ограничение может принимать в качестве выделения следующие примитивы:\n" +"\n" +" * два или более отрезка (равенство длин отрезков)\n" +" * два отрезка и две точки (равенство расстояний от точек до линий)\n" +" * отрезок и две точки (равенство расстояний от точек до линии)\n" +" * отрезок, точку и отрезок (равенство длины отрезка расстоянию от точки " +"до линии)\n" +" * две или более окружностей / дуги (равенство радиусов)\n" +" * отрезок и дугу (равенство длины отрезка и длины дуги)\n" + +#: constraint.cpp:480 +msgid "" +"Bad selection for length ratio constraint. This constraint can apply to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"Неправильное выделение для ограничения 'отношение длин'. Ограничение может " +"принимать в качестве выделения следующие примитивы:\n" +"\n" +" * два отрезка\n" +" * две дуги\n" +" * дугу и отрезок\n" + +#: constraint.cpp:515 +msgid "" +"Bad selection for length difference constraint. This constraint can apply " +"to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"Неправильное выделение для ограничения 'разность длин'. Ограничение может " +"принимать в качестве выделения следующие примитивы:\n" +"\n" +" * два отрезка\n" +" * две дуги\n" +" * дугу и отрезок\n" + +#: constraint.cpp:550 +msgid "" +"Bad selection for at midpoint constraint. This constraint can apply to:\n" +"\n" +" * a line segment and a point (point at midpoint)\n" +" * a line segment and a workplane (line's midpoint on plane)\n" +msgstr "" +"Неправильное выделение для ограничения 'на середине'.\n" +"Ограничение может принимать в качестве выделения следующие примитивы:\n" +"\n" +" * точку и отрезок (точка на середине отрезка)\n" +" * отрезок и рабочую плоскость (середина отрезка на плоскости)\n" + +#: constraint.cpp:608 +msgid "" +"Bad selection for symmetric constraint. This constraint can apply to:\n" +"\n" +" * two points or a line segment (symmetric about workplane's coordinate " +"axis)\n" +" * line segment, and two points or a line segment (symmetric about line " +"segment)\n" +" * workplane, and two points or a line segment (symmetric about " +"workplane)\n" +msgstr "" +"Неправильное выделение для ограничения 'симметрия'.\n" +"Ограничение может принимать в качестве выделения следующие примитивы:\n" +"\n" +" * две точки / отрезок (симметричность точек по горизонтали/вертикали в " +"зависимости от вида)\n" +" * отрезок, две точки / отрезок (симметричность точек по оси отрезка)\n" +" * рабочую плоскость и две точки / отрезок (симметричность относительно " +"рабочей плоскости\n" + +#: constraint.cpp:623 +msgid "" +"A workplane must be active when constraining symmetric without an explicit " +"symmetry plane." +msgstr "" +"Рабочая плоскость должна быть активна для того, чтобы создать\n" +"ограничение симметричности без явного указания плоскости симметрии." + +#: constraint.cpp:663 +msgid "" +"Activate a workplane (with Sketch -> In Workplane) before applying a " +"horizontal or vertical constraint." +msgstr "" +"Рабочая плоскость должна быть активирована (Эскиз -> В рабочей плоскости)\n" +"перед тем, как накладывать ограничения горизонтальности / вертикальности." + +#: constraint.cpp:679 +msgid "" +"Bad selection for horizontal / vertical constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points\n" +" * one or more line segments\n" +msgstr "" +"Неправильное выделение для ограничения 'горизонтальность / вертикальность'.\n" +"Ограничение может принимать в качестве выделения следующие примитивы:\n" +"\n" +" * две или более точек\n" +" * один или более отрезков\n" + +#: constraint.cpp:697 +msgid "" +"Bad selection for same orientation constraint. This constraint can apply " +"to:\n" +"\n" +" * two normals\n" +msgstr "" +"Неправильное выделение для ограничения 'идентичная ориентация'.\n" +"Ограничение может принимать в качестве выделения следующие примитивы:\n" +"\n" +" * два координатных базиса('нормали')\n" + +#: constraint.cpp:748 +msgid "Must select an angle constraint." +msgstr "" +"Переключатся между смежными углами можно только выбрав ограничение угла." + +#: constraint.cpp:761 +msgid "Must select a constraint with associated label." +msgstr "" +"Переключать режим 'размера для справок' возможно только для ограничений, " +"имеющих размерное значение." + +#: constraint.cpp:784 +msgid "" +"Bad selection for angle constraint. This constraint can apply to:\n" +"\n" +"Angle between:\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +"\n" +"Equal angles:\n" +" * four line segments or normals (equal angle between A,B and C,D)\n" +" * three line segments or normals (equal angle between A,B and B,C)\n" +msgstr "" +"Неправильное выделение для ограничения углов.\n" +"Ограничение может принимать в качестве выделения следующие примитивы:\n" +"\n" +"Угол между:\n" +" * два отрезка\n" +" * отрезок и координатный базис (нормаль)\n" +" * два координатных базиса (нормали)\n" +"\n" +"Равные углы:\n" +" * четыре отрезка или нормали (равенство углов A,B и C,D)\n" +" * три отрезка или нормали (равенство углов A,B and B,C)\n" + +#: constraint.cpp:872 +msgid "Curve-curve tangency must apply in workplane." +msgstr "" +"Ограничение касательности может быть наложено только в рабочей плоскости." + +#: constraint.cpp:887 +msgid "" +"Bad selection for parallel / tangent constraint. This constraint can apply " +"to:\n" +"\n" +" * two faces\n" +" * two or more line segments (parallel)\n" +" * one or more line segments and one or more normals (parallel)\n" +" * two or more normals (parallel)\n" +" * two line segments, arcs, or beziers, that share an endpoint (tangent)\n" +" * two line segments, arcs, or beziers, that do not share an endpoint and " +"the end point(s) of the curve(s) (tangent)\n" +msgstr "" +"Неправильное выделение для ограничения параллельности / касательности.\n" +"Ограничение может принимать в качестве выделения следующие примитивы:\n" +"\n" +" * две граней\n" +" * два или более отрезка (параллельность)\n" +" * два или более отрезка и одинь или более координатных базисов (нормали) " +"(параллельность)\n" +" * два или более координатных базиса (нормали) (параллельность)\n" +" * два отрезка, две дуги или два сплайна, соединенных крайними точками " +"(касательность)\n" +" * два отрезка, две дуги или два сплайна, не имеющие общей крайней точки," +"но с выбранными крайними точками (касательность)\n" + +#: constraint.cpp:914 +msgid "" +"Bad selection for perpendicular constraint. This constraint can apply to:\n" +"\n" +" * two faces\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +msgstr "" +"Неправильное выделение для ограничения перпендикулярности.\n" +"Ограничение может принимать в качестве выделения следующие примитивы:\n" +"\n" +" * две граней\n" +" * два отрезка\n" +" * отрезок и координатный базис (нормаль)\n" +" * два координатных базиса (нормали)\n" + +#: constraint.cpp:931 +msgid "" +"Bad selection for lock point where dragged constraint. This constraint can " +"apply to:\n" +"\n" +" * a point\n" +msgstr "" +"Неправильное выделение для ограничения 'фиксация'.\n" +"Ограничение может принимать в качестве выделения следующие примитивы:\n" +"\n" +" * точку\n" + +#: constraint.cpp:946 mouse.cpp:1160 +msgid "NEW COMMENT -- DOUBLE-CLICK TO EDIT" +msgstr "КОММЕНТАРИЙ -- ДВОЙНОЙ ЩЕЛЧОК ДЛЯ РЕДАКТИРОВАНИЯ" + +#: constraint.cpp:952 +msgid "click center of comment text" +msgstr "кликните мышью там, где будет расположен текстовый комментарий" + +#: export.cpp:19 +msgid "" +"No solid model present; draw one with extrudes and revolves, or use Export " +"2d View to export bare lines and curves." +msgstr "" +"Тела не найдены; создайте их или используйте экспорт двумерных объектов." + +#: export.cpp:61 +msgid "" +"Bad selection for export section. Please select:\n" +"\n" +" * nothing, with an active workplane (workplane is section plane)\n" +" * a face (section plane through face)\n" +" * a point and two line segments (plane through point and parallel to " +"lines)\n" +msgstr "" +"Неправильное выделение для экспорта сечения. Необходимо выделить:\n" +"\n" +" * Ничего, но с активированной рабочей плоскостью (рабочая плоскость " +"будет плоскостью сечения)\n" +" * грань (сечение через грань)\n" +" * точку и два отрезка (сечение плоскостью, заданной двумя отрезками, " +"построенной через указанную точку)\n" + +#: export.cpp:818 +msgid "Active group mesh is empty; nothing to export." +msgstr "Активная группа не содержит тел; нечего экспортировать." + +#: exportvector.cpp:336 +msgid "freehand lines were replaced with continuous lines" +msgstr "стили линии 'от руки' были заменены сплошными линиями" + +#: exportvector.cpp:338 +msgid "zigzag lines were replaced with continuous lines" +msgstr "ломаные линии были заменены сплошными линиями" + +#: exportvector.cpp:592 +msgid "" +"Some aspects of the drawing have no DXF equivalent and were not exported:\n" +msgstr "" +"Некоторые элементы чертежа не имеют аналогов в DXF-представлении и не были " +"экспортированы:\n" + +#: exportvector.cpp:838 +msgid "" +"PDF page size exceeds 200 by 200 inches; many viewers may reject this file." +msgstr "" +"Размер страницы PDF превышает 200x200 дюймов; некоторые программы просмотра " +"не смогут прочитать такой файл." + +#: file.cpp:44 group.cpp:91 +msgctxt "group-name" +msgid "sketch-in-plane" +msgstr "эскиз-в-плоскости" + +#: file.cpp:62 +msgctxt "group-name" +msgid "#references" +msgstr "система-координат" + +#: file.cpp:555 +msgid "The file is empty. It may be corrupt." +msgstr "Файл пуст. Возможно он поврежден." + +#: file.cpp:560 +msgid "" +"Unrecognized data in file. This file may be corrupt, or from a newer version " +"of the program." +msgstr "" +"Некоторые данные из этого файла не распознаны. Возможно, файл поврежден или " +"создан в более новой версии программы." + +#: file.cpp:876 +msgctxt "title" +msgid "Missing File" +msgstr "Файл Отсутствует" + +#: file.cpp:877 +#, c-format +msgctxt "dialog" +msgid "The linked file “%s” is not present." +msgstr "Связанный файл “%s” отсутствует." + +#: file.cpp:879 +msgctxt "dialog" +msgid "" +"Do you want to locate it manually?\n" +"\n" +"If you decline, any geometry that depends on the missing file will be " +"permanently removed." +msgstr "" +"Хотите найти их вручную?\n" +"Если вы ответите \"Нет\", то вся геометрия, которая зависит от " +"отсутствующего файла будет удалена." + +#: file.cpp:882 +msgctxt "button" +msgid "&Yes" +msgstr "Да" + +#: file.cpp:884 +msgctxt "button" +msgid "&No" +msgstr "Нет" + +#: file.cpp:886 solvespace.cpp:652 +msgctxt "button" +msgid "&Cancel" +msgstr "Отменить" + +#: graphicswin.cpp:41 +msgid "&File" +msgstr "&Файл" + +#: graphicswin.cpp:42 +msgid "&New" +msgstr "Соз&дать" + +#: graphicswin.cpp:43 +msgid "&Open..." +msgstr "&Открыть..." + +#: graphicswin.cpp:44 +msgid "Open &Recent" +msgstr "Открыть н&едавние файлы" + +#: graphicswin.cpp:45 +msgid "&Save" +msgstr "&Сохранить" + +#: graphicswin.cpp:46 +msgid "Save &As..." +msgstr "Сохранить &Как…" + +#: graphicswin.cpp:48 +msgid "Export &Image..." +msgstr "Экспортировать и&зображение…" + +#: graphicswin.cpp:49 +msgid "Export 2d &View..." +msgstr "Экспортировать &2d-вид…" + +#: graphicswin.cpp:50 +msgid "Export 2d &Section..." +msgstr "Экспортировать 2d-се&чение…" + +#: graphicswin.cpp:51 +msgid "Export 3d &Wireframe..." +msgstr "Экспортировать &3d-каркас…" + +#: graphicswin.cpp:52 +msgid "Export Triangle &Mesh..." +msgstr "Экспортировать &полигональную сетку…" + +#: graphicswin.cpp:53 +msgid "Export &Surfaces..." +msgstr "Экспортировать повер&хности…" + +#: graphicswin.cpp:54 +msgid "Im&port..." +msgstr "&Импортировать..." + +#: graphicswin.cpp:57 +msgid "E&xit" +msgstr "&Выход" + +#: graphicswin.cpp:60 +msgid "&Edit" +msgstr "&Правка" + +#: graphicswin.cpp:61 +msgid "&Undo" +msgstr "&Отменить" + +#: graphicswin.cpp:62 +msgid "&Redo" +msgstr "&Вернуть" + +#: graphicswin.cpp:63 +msgid "Re&generate All" +msgstr "&Перегенерировать все" + +#: graphicswin.cpp:65 +msgid "Snap Selection to &Grid" +msgstr "Привязать выделение к &сетке" + +#: graphicswin.cpp:66 +msgid "Rotate Imported &90°" +msgstr "Повернуть импортированное на &90°" + +#: graphicswin.cpp:68 +msgid "Cu&t" +msgstr "Вы&резать" + +#: graphicswin.cpp:69 +msgid "&Copy" +msgstr "&Копировать" + +#: graphicswin.cpp:70 +msgid "&Paste" +msgstr "В&ставить" + +#: graphicswin.cpp:71 +msgid "Paste &Transformed..." +msgstr "Вставить с &трансформацией…" + +#: graphicswin.cpp:72 +msgid "&Delete" +msgstr "&Удалить" + +#: graphicswin.cpp:74 +msgid "Select &Edge Chain" +msgstr "Вы&делить последовательность ребер" + +#: graphicswin.cpp:75 +msgid "Select &All" +msgstr "В&ыделить всё" + +#: graphicswin.cpp:76 +msgid "&Unselect All" +msgstr "С&бросить выделение" + +#: graphicswin.cpp:78 +msgid "&Line Styles..." +msgstr "Стили линий…" + +#: graphicswin.cpp:79 +msgid "&View Projection..." +msgstr "&Проекция вида..." + +#: graphicswin.cpp:81 +msgid "Con&figuration..." +msgstr "Настройки..." + +#: graphicswin.cpp:84 +msgid "&View" +msgstr "&Вид" + +#: graphicswin.cpp:85 +msgid "Zoom &In" +msgstr "&Приблизить" + +#: graphicswin.cpp:86 +msgid "Zoom &Out" +msgstr "От&далить" + +#: graphicswin.cpp:87 +msgid "Zoom To &Fit" +msgstr "Показать &всё / выделенное" + +#: graphicswin.cpp:89 +msgid "Align View to &Workplane" +msgstr "Выровнять вид по &рабочей плоскости" + +#: graphicswin.cpp:90 +msgid "Nearest &Ortho View" +msgstr "Ближайший &ортогональный вид" + +#: graphicswin.cpp:91 +msgid "Nearest &Isometric View" +msgstr "Ближайший &изометрический вид" + +#: graphicswin.cpp:92 +msgid "&Center View At Point" +msgstr "&Центрировать вид на точке" + +#: graphicswin.cpp:94 +msgid "Show Snap &Grid" +msgstr "Показать &сетку" + +#: graphicswin.cpp:95 +msgid "Darken Inactive Solids" +msgstr "Затемнять неактивные тела" + +#: graphicswin.cpp:96 +msgid "Use &Perspective Projection" +msgstr "Перспективная прое&кция" + +#: graphicswin.cpp:97 +msgid "Show E&xploded View" +msgstr "Показать &развернутый вид" + +#: graphicswin.cpp:98 +msgid "Dimension &Units" +msgstr "Единицы &измерения" + +#: graphicswin.cpp:99 +msgid "Dimensions in &Millimeters" +msgstr "Размеры в ми&ллиметрах" + +#: graphicswin.cpp:100 +msgid "Dimensions in M&eters" +msgstr "Размеры в метрах" + +#: graphicswin.cpp:101 +msgid "Dimensions in &Inches" +msgstr "Размеры в дю&ймах" + +#: graphicswin.cpp:102 +msgid "Dimensions in &Feet and Inches" +msgstr "Размеры в &футах и дюймах" + +#: graphicswin.cpp:104 +msgid "Show &Toolbar" +msgstr "Показывать па&нель инструментов" + +#: graphicswin.cpp:105 +msgid "Show Property Bro&wser" +msgstr "Показывать брау&зер" + +#: graphicswin.cpp:107 +msgid "&Full Screen" +msgstr "Полно&экранный режим" + +#: graphicswin.cpp:109 +msgid "&New Group" +msgstr "&Новая группа" + +#: graphicswin.cpp:110 +msgid "Sketch In &3d" +msgstr "Создать эскиз в &3d" + +#: graphicswin.cpp:111 +msgid "Sketch In New &Workplane" +msgstr "Создать эскиз в новой &рабочей плоскости" + +#: graphicswin.cpp:113 +msgid "Step &Translating" +msgstr "&Линейный массив" + +#: graphicswin.cpp:114 +msgid "Step &Rotating" +msgstr "&Круговой массив" + +#: graphicswin.cpp:116 +msgid "E&xtrude" +msgstr "Тело &выдавливания" + +#: graphicswin.cpp:117 +msgid "&Helix" +msgstr "Тело в&интовое" + +#: graphicswin.cpp:118 +msgid "&Lathe" +msgstr "Тело в&ращения" + +#: graphicswin.cpp:119 +msgid "Re&volve" +msgstr "Тело в&ращения на угол" + +#: graphicswin.cpp:121 +msgid "Link / Assemble..." +msgstr "&Импортировать деталь/сборку…" + +#: graphicswin.cpp:122 +msgid "Link Recent" +msgstr "Импортировать недавний файл" + +#: graphicswin.cpp:124 +msgid "&Sketch" +msgstr "&Эскиз" + +#: graphicswin.cpp:125 +msgid "In &Workplane" +msgstr "В &рабочей плоскости" + +#: graphicswin.cpp:126 +msgid "Anywhere In &3d" +msgstr "Режим &3d" + +#: graphicswin.cpp:128 +msgid "Datum &Point" +msgstr "Опорная &точка" + +#: graphicswin.cpp:129 +msgid "Wor&kplane" +msgstr "Рабочая &плоскость" + +#: graphicswin.cpp:131 +msgid "Line &Segment" +msgstr "&Отрезок" + +#: graphicswin.cpp:132 +msgid "C&onstruction Line Segment" +msgstr "&Вспомогательный отрезок" + +#: graphicswin.cpp:133 +msgid "&Rectangle" +msgstr "Прямоу&гольник" + +#: graphicswin.cpp:134 +msgid "&Circle" +msgstr "О&кружность" + +#: graphicswin.cpp:135 +msgid "&Arc of a Circle" +msgstr "Д&уга окружности" + +#: graphicswin.cpp:136 +msgid "&Bezier Cubic Spline" +msgstr "Кубический &сплайн Безье" + +#: graphicswin.cpp:138 +msgid "&Text in TrueType Font" +msgstr "Т&екст TrueType" + +#: graphicswin.cpp:139 +msgid "I&mage" +msgstr "И&зображение" + +#: graphicswin.cpp:141 +msgid "To&ggle Construction" +msgstr "Переключить режим вс&помогательных построений" + +#: graphicswin.cpp:142 +msgid "Ta&ngent Arc at Point" +msgstr "Кас&ательная в точке" + +#: graphicswin.cpp:143 +msgid "Split Curves at &Intersection" +msgstr "Ра&збить кривые пересечением" + +#: graphicswin.cpp:145 +msgid "&Constrain" +msgstr "&Ограничение" + +#: graphicswin.cpp:146 +msgid "&Distance / Diameter" +msgstr "&Расстояние / Диаметр" + +#: graphicswin.cpp:147 +msgid "Re&ference Dimension" +msgstr "Справочный ра&змер" + +#: graphicswin.cpp:148 +msgid "A&ngle / Equal Angle" +msgstr "&Угол / Равенство углов" + +#: graphicswin.cpp:149 +msgid "Reference An&gle" +msgstr "Справочный &угол" + +#: graphicswin.cpp:150 +msgid "Other S&upplementary Angle" +msgstr "Переключить сме&жный угол" + +#: graphicswin.cpp:151 +msgid "Toggle R&eference Dim" +msgstr "Переключить &режим справочного размера" + +#: graphicswin.cpp:153 +msgid "&Horizontal" +msgstr "&Горизонтальность" + +#: graphicswin.cpp:154 +msgid "&Vertical" +msgstr "&Вертикальность" + +#: graphicswin.cpp:156 +msgid "&On Point / Curve / Plane" +msgstr "&Точка на примитиве" + +#: graphicswin.cpp:157 +msgid "E&qual Length / Radius" +msgstr "&Равенство длин / радиусов" + +#: graphicswin.cpp:158 +msgid "Length / Arc Ra&tio" +msgstr "&Отношение длин (дуга)" + +#: graphicswin.cpp:159 +msgid "Length / Arc Diff&erence" +msgstr "Р&азность длин (дуга)" + +#: graphicswin.cpp:160 +msgid "At &Midpoint" +msgstr "&На середине" + +#: graphicswin.cpp:161 +msgid "S&ymmetric" +msgstr "С&имметричность" + +#: graphicswin.cpp:162 +msgid "Para&llel / Tangent" +msgstr "Пара&ллельность / Касательность" + +#: graphicswin.cpp:163 +msgid "&Perpendicular" +msgstr "Перпендикул&ярность" + +#: graphicswin.cpp:164 +msgid "Same Orient&ation" +msgstr "Идентичная &ориентация" + +#: graphicswin.cpp:165 +msgid "Lock Point Where &Dragged" +msgstr "За&фиксировать" + +#: graphicswin.cpp:167 +msgid "Comment" +msgstr "Комментарий" + +#: graphicswin.cpp:169 +msgid "&Analyze" +msgstr "&Анализ" + +#: graphicswin.cpp:170 +msgid "Measure &Volume" +msgstr "Измерить &объем" + +#: graphicswin.cpp:171 +msgid "Measure A&rea" +msgstr "Измерить п&лощадь" + +#: graphicswin.cpp:172 +msgid "Measure &Perimeter" +msgstr "Измерить п&ериметр" + +#: graphicswin.cpp:173 +msgid "Show &Interfering Parts" +msgstr "Показать пе&ресекающиеся детали" + +#: graphicswin.cpp:174 +msgid "Show &Naked Edges" +msgstr "Показать про&блемные ребра" + +#: graphicswin.cpp:175 +msgid "Show &Center of Mass" +msgstr "Показать &центр массы" + +#: graphicswin.cpp:177 +msgid "Show &Underconstrained Points" +msgstr "Показать c&вободные точки" + +#: graphicswin.cpp:179 +msgid "&Trace Point" +msgstr "Включить &трассировку точки" + +#: graphicswin.cpp:180 +msgid "&Stop Tracing..." +msgstr "Остановить тра&ссировку..." + +#: graphicswin.cpp:181 +msgid "Step &Dimension..." +msgstr "Плавное из&менение размера…" + +#: graphicswin.cpp:183 +msgid "&Help" +msgstr "&Справка" + +#: graphicswin.cpp:184 +msgid "&Language" +msgstr "&Язык" + +#: graphicswin.cpp:185 +msgid "&Website / Manual" +msgstr "Вебсайт / &Справка" + +#: graphicswin.cpp:186 +msgid "&Go to GitHub commit" +msgstr "Пере&йти к коммиту на GitHub" + +#: graphicswin.cpp:188 +msgid "&About" +msgstr "О &Программе" + +#: graphicswin.cpp:362 +msgid "(no recent files)" +msgstr "(пусто)" + +#: graphicswin.cpp:370 +#, c-format +msgid "File '%s' does not exist." +msgstr "Файл '%s' не существует." + +#: graphicswin.cpp:779 +msgid "No workplane is active, so the grid will not appear." +msgstr "Сетку не будет видно, пока рабочая плоскость не активирована." + +#: graphicswin.cpp:794 +msgid "" +"The perspective factor is set to zero, so the view will always be a parallel " +"projection.\n" +"\n" +"For a perspective projection, modify the perspective factor in the " +"configuration screen. A value around 0.3 is typical." +msgstr "" +"Коэффициент перспективы установлен в ноль, что соответствует ортогональной " +"проекции.\n" +"Чтобы получить перспективную проекцию, отредактируйте значение коэффициента " +"перспективы на конфигурационной странице браузера.\n" +"Значение по умолчанию 0.3." + +#: graphicswin.cpp:884 +msgid "" +"Select a point; this point will become the center of the view on screen." +msgstr "Выделите точку. Вид будет отцентрован по этой точке." + +#: graphicswin.cpp:1193 +msgid "No additional entities share endpoints with the selected entities." +msgstr "Нет дополнительных объектов, соединенных с выбранными примитивами." + +#: graphicswin.cpp:1211 +msgid "" +"To use this command, select a point or other entity from an linked part, or " +"make a link group the active group." +msgstr "" +"Чтобы использовать эту команду, выделите точку или другой примитив, " +"принадлежащий импортированной детали, или активируйте группу импортированной " +"детали." + +#: graphicswin.cpp:1234 +msgid "" +"No workplane is active. Activate a workplane (with Sketch -> In Workplane) " +"to define the plane for the snap grid." +msgstr "" +"Рабочая плоскость не активна. Активируйте ее через Эскиз -> В Рабочей " +"Плоскости чтобы определить плоскость для сетки." + +#: graphicswin.cpp:1241 +msgid "" +"Can't snap these items to grid; select points, text comments, or constraints " +"with a label. To snap a line, select its endpoints." +msgstr "" +"Невозможно привязать выбранные примитивы к сетке. Необходимо выбирать точки, " +"текстовые комментарии или ограничения с размерными значениями. Чтобы " +"привязать отрезок или другой примитив, выбирайте его точки." + +#: graphicswin.cpp:1326 +msgid "No workplane selected. Activating default workplane for this group." +msgstr "" +"Рабочая плоскость не активна. Активирована рабочая плоскость по умолчанию " +"для данной группы." + +#: graphicswin.cpp:1329 +msgid "" +"No workplane is selected, and the active group does not have a default " +"workplane. Try selecting a workplane, or activating a sketch-in-new-" +"workplane group." +msgstr "" +"Рабочая плоскость не выбрана и активная группа не содержит рабочей плоскости " +"по умолчанию. Попробуйте выделить рабочую плоскость или создать новую с " +"помощью Группа -> Создать Эскиз в Новой Рабочей Плоскости." + +#: graphicswin.cpp:1350 +msgid "" +"Bad selection for tangent arc at point. Select a single point, or select " +"nothing to set up arc parameters." +msgstr "" +"Неправильное выделение для создания скругления в точке. Выделите либо одну " +"точку, либо запустите команду без выделения, чтобы перейти к окну настроек " +"этой команды." + +#: graphicswin.cpp:1361 +msgid "click point on arc (draws anti-clockwise)" +msgstr "" +"кликните мышью там, где хотите создать дугу окружности (дуга будет " +"нарисована против часовой стрелки)" + +#: graphicswin.cpp:1362 +msgid "click to place datum point" +msgstr "кликните мышью там, где хотите создать опорную точку" + +#: graphicswin.cpp:1363 +msgid "click first point of line segment" +msgstr "кликните мышью там, где хотите создать первую точку отрезка" + +#: graphicswin.cpp:1365 +msgid "click first point of construction line segment" +msgstr "" +"кликните мышью там, где хотите создать первую точку вспомогательного отрезка" + +#: graphicswin.cpp:1366 +msgid "click first point of cubic segment" +msgstr "" +"кликните мышью там, где хотите создать первую точку кубического сплайна Безье" + +#: graphicswin.cpp:1367 +msgid "click center of circle" +msgstr "кликните мышью там, где будет находиться центр окружности" + +#: graphicswin.cpp:1368 +msgid "click origin of workplane" +msgstr "" +"кликните мышью там, где будет находиться точка, через которую будет " +"построена рабочая плоскость" + +#: graphicswin.cpp:1369 +msgid "click one corner of rectangle" +msgstr "кликните мышью там, где будет находиться один из углов прямоугольника" + +#: graphicswin.cpp:1370 +msgid "click top left of text" +msgstr "кликните мышью там, где хотите создать текст" + +#: graphicswin.cpp:1376 +msgid "click top left of image" +msgstr "" +"кликните мышью там, где будет расположен левый верхний угол изображения" + +#: graphicswin.cpp:1402 +msgid "" +"No entities are selected. Select entities before trying to toggle their " +"construction state." +msgstr "" +"Не выбран ни один примитив. Выберите примитивы перед тем, как переключать их " +"режим дополнительных построений." + +#: group.cpp:86 +msgctxt "group-name" +msgid "sketch-in-3d" +msgstr "эскиз-в-3d" + +#: group.cpp:154 +msgid "" +"Bad selection for new sketch in workplane. This group can be created with:\n" +"\n" +" * a point (through the point, orthogonal to coordinate axes)\n" +" * a point and two line segments (through the point, parallel to the " +"lines)\n" +" * a point and a normal (through the point, orthogonal to the normal)\n" +" * a workplane (copy of the workplane)\n" +msgstr "" +"Неправильное выделение для создания эскиза в рабочей плоскости. Группа может " +"быть создана, используя в качестве выделения следующие примитивы:\n" +"\n" +" * точку (через точку, ортогонально осям координат)\n" +" * точку и два отрезка (через точку, параллельно отрезкам)\n" +" * точку и нормаль (через точку, ортогонально нормали)\n" +" * рабочую плоскость (копию рабочей плоскости)\n" + +#: group.cpp:170 +msgid "" +"Activate a workplane (Sketch -> In Workplane) before extruding. The sketch " +"will be extruded normal to the workplane." +msgstr "" +"Выберите рабочую плоскость (Эскиз -> В Рабочей Плоскости) перед созданием " +"группы выдавливания. Эскиз будет выдавлен по нормали к рабочей плоскости." + +#: group.cpp:179 +msgctxt "group-name" +msgid "extrude" +msgstr "тело-выдавливания" + +#: group.cpp:184 +msgid "Lathe operation can only be applied to planar sketches." +msgstr "" +"Операция создания тела вращения может быть применена только к плоским " +"эскизам." + +#: group.cpp:195 +msgid "" +"Bad selection for new lathe group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Неправильное выделение для создания группы тела вращения. \n" +"Группа может быть создана, используя в качестве выделения следующие " +"примитивы:\n" +"\n" +" * точку и отрезок / координатный базис (нормаль) (вращение вокруг оси, " +"проходящей через точку и параллельной отрезку / нормали)\n" +" * отрезок (вращение вокруг оси, проходящей через отрезок)\n" +"\n" + +#: group.cpp:205 +msgctxt "group-name" +msgid "lathe" +msgstr "тело-вращения" + +#: group.cpp:210 +msgid "Revolve operation can only be applied to planar sketches." +msgstr "" +"Операция создания тела вращения на угол может быть применена только к " +"плоским эскизам." + +#: group.cpp:221 +msgid "" +"Bad selection for new revolve group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Неправильное выделение для создания группы тела вращения на угол. \n" +"Группа может быть создана, используя в качестве выделения следующие " +"примитивы:\n" +"\n" +" * точку и отрезок / координатный базис (нормаль) (вращение вокруг оси, " +"проходящей через точку и параллельной отрезку / нормали)\n" +" * отрезок (вращение вокруг оси, проходящей через отрезок)\n" +"\n" + +#: group.cpp:233 +msgctxt "group-name" +msgid "revolve" +msgstr "тело-вращения-на-угол" + +#: group.cpp:238 +msgid "Helix operation can only be applied to planar sketches." +msgstr "" +"Операция создания винтового тела может быть применена только к плоским " +"эскизам." + +#: group.cpp:249 +msgid "" +"Bad selection for new helix group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Неправильное выделение для создания винтового тела.\n" +"Группа может быть создана, используя в качестве выделения следующие " +"примитивы:\n" +"\n" +" * точку и отрезок или координатный базис (нормаль) (вращение вокруг " +"направления, параллельного линии / нормали, построенное через выбранную " +"точку)\n" +" * отрезок (вращение вокруг отрезка)\n" + +#: group.cpp:261 +msgctxt "group-name" +msgid "helix" +msgstr "тело-винтовое" + +#: group.cpp:274 +msgid "" +"Bad selection for new rotation. This group can be created with:\n" +"\n" +" * a point, while locked in workplane (rotate in plane, about that " +"point)\n" +" * a point and a line or a normal (rotate about an axis through the " +"point, and parallel to line / normal)\n" +msgstr "" +"Неправильное выделение для создания группы кругового массива. \n" +"Группа может быть создана, используя в качестве выделения следующие " +"примитивы:\n" +"\n" +" * точку при активной рабочей плоскости (вращение в плоскости вокруг " +"выбранной точки)\n" +" * точку и отрезок / координатный базис (нормаль) (вращение вокруг оси, " +"проходящей через точку и параллельной отрезку / нормали)\n" +"\n" + +#: group.cpp:287 +msgctxt "group-name" +msgid "rotate" +msgstr "круговой-массив" + +#: group.cpp:298 +msgctxt "group-name" +msgid "translate" +msgstr "линейный-массив" + +#: group.cpp:422 +msgid "(unnamed)" +msgstr "(без имени)" + +#: groupmesh.cpp:710 +msgid "not closed contour, or not all same style!" +msgstr "незамкнутый контур или несовпадение стилей!" + +#: groupmesh.cpp:723 +msgid "points not all coplanar!" +msgstr "не все точки лежат в одной плоскости!" + +#: groupmesh.cpp:725 +msgid "contour is self-intersecting!" +msgstr "контур имеет самопересечения!" + +#: groupmesh.cpp:727 +msgid "zero-length edge!" +msgstr "ребро нулевой длины!" + +#: importmesh.cpp:136 +msgid "Text-formated STL files are not currently supported" +msgstr "Текстовые файлы STL пока не поддерживаются" + +#: modify.cpp:252 +msgid "Must be sketching in workplane to create tangent arc." +msgstr "" +"Скругления эскиза можно создавать только когда рабочая плоскость " +"активирована." + +#: modify.cpp:299 +msgid "" +"To create a tangent arc, select a point where two non-construction lines or " +"circles in this group and workplane join." +msgstr "" +"Чтобы создать скругление эскиза, выберите точку, где соединяются два " +"примитива, не принадлежащих к вспомогательной геометрии." + +#: modify.cpp:386 +msgid "" +"Couldn't round this corner. Try a smaller radius, or try creating the " +"desired geometry by hand with tangency constraints." +msgstr "" +"Невозможно скруглить угол. Попробуйте радиус поменьше или создайте требуемую " +"геометрию с помощью Ограничения -> Параллельность / Касательность." + +#: modify.cpp:595 +msgid "Couldn't split this entity; lines, circles, or cubics only." +msgstr "" +"Невозможно разделить такие примитивы. Выберите линии, окружности или " +"кубические сплайны." + +#: modify.cpp:622 +msgid "Must be sketching in workplane to split." +msgstr "" +"Пересечение примитивов работает только когда рабочая плоскость активна." + +#: modify.cpp:629 +msgid "" +"Select two entities that intersect each other (e.g. two lines/circles/arcs " +"or a line/circle/arc and a point)." +msgstr "" +"Выберите два пересекающихся примитива (два отрезка/окружности/дуги или " +"отрезок/окружность/дугу и точку)" + +#: modify.cpp:734 +msgid "Can't split; no intersection found." +msgstr "Невозможно разделить пересекаемые примитивы: пересечений нет." + +#: mouse.cpp:558 +msgid "Assign to Style" +msgstr "Назначить стилю" + +#: mouse.cpp:574 +msgid "No Style" +msgstr "Без стиля" + +#: mouse.cpp:577 +msgid "Newly Created Custom Style..." +msgstr "Создать новый стиль…" + +#: mouse.cpp:584 +msgid "Group Info" +msgstr "Настройки группы" + +#: mouse.cpp:604 +msgid "Style Info" +msgstr "Настройки стиля" + +#: mouse.cpp:624 +msgid "Select Edge Chain" +msgstr "Выделить последовательность ребер" + +#: mouse.cpp:630 +msgid "Toggle Reference Dimension" +msgstr "Переключить справочный размер" + +#: mouse.cpp:636 +msgid "Other Supplementary Angle" +msgstr "Переключить смежный угол" + +#: mouse.cpp:641 +msgid "Snap to Grid" +msgstr "Привязать к сетке" + +#: mouse.cpp:650 +msgid "Remove Spline Point" +msgstr "Удалить точку сплайна" + +#: mouse.cpp:685 +msgid "Add Spline Point" +msgstr "Добавить точку сплайна" + +#: mouse.cpp:689 +msgid "Cannot add spline point: maximum number of points reached." +msgstr "" +"Невозможно добавить точку сплайна: достигнуто ограничение максимального " +"количества точек для сплайна." + +#: mouse.cpp:714 +msgid "Toggle Construction" +msgstr "Переключить режим «Дополнительные построения»" + +#: mouse.cpp:730 +msgid "Delete Point-Coincident Constraint" +msgstr "Удалить ограничение совпадения точек" + +#: mouse.cpp:748 +msgid "Cut" +msgstr "Вырезать" + +#: mouse.cpp:750 +msgid "Copy" +msgstr "Копировать" + +#: mouse.cpp:754 +msgid "Select All" +msgstr "Выделить всё" + +#: mouse.cpp:759 +msgid "Paste" +msgstr "Вставить" + +#: mouse.cpp:761 +msgid "Paste Transformed..." +msgstr "Вставить с трансформацией…" + +#: mouse.cpp:766 +msgid "Delete" +msgstr "Удалить" + +#: mouse.cpp:769 +msgid "Unselect All" +msgstr "Сбросить выделение" + +#: mouse.cpp:776 +msgid "Unselect Hovered" +msgstr "Снять выделение с выбранного" + +#: mouse.cpp:785 +msgid "Zoom to Fit" +msgstr "Показать всё" + +#: mouse.cpp:987 mouse.cpp:1276 +msgid "click next point of line, or press Esc" +msgstr "кликните мышью там, где хотите расположить следующую точку" + +#: mouse.cpp:993 +msgid "" +"Can't draw rectangle in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Невозможно начертить прямоугольник, когда рабочая плоскость не активна." + +#: mouse.cpp:1027 +msgid "click to place other corner of rectangle" +msgstr "кликните мышью там, где хотите расположить другой угол прямоугольника" + +#: mouse.cpp:1048 +msgid "click to set radius" +msgstr "кликните, чтобы задать радиус" + +#: mouse.cpp:1053 +msgid "" +"Can't draw arc in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "Невозможно создать дугу, когда нет активной рабочей плоскости." + +#: mouse.cpp:1072 +msgid "click to place point" +msgstr "кликните мышью там, где хотите создать точку" + +#: mouse.cpp:1088 +msgid "click next point of cubic, or press Esc" +msgstr "кликните следующую точку сплайна или нажмите Esc" + +#: mouse.cpp:1093 +msgid "" +"Sketching in a workplane already; sketch in 3d before creating new workplane." +msgstr "" +"Рабочая плоскость уже активна. Перейдите в режим 3d перед созданием новой " +"рабочей плоскости." + +#: mouse.cpp:1109 +msgid "" +"Can't draw text in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "Невозможно создать текст, когда нет активной рабочей плоскости." + +#: mouse.cpp:1126 +msgid "click to place bottom right of text" +msgstr "кликните, чтобы расположить правый нижний угол текста" + +#: mouse.cpp:1132 +msgid "" +"Can't draw image in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "Невозможно создать изображение. Активируйте рабочую плоскость." + +#: platform/gui.cpp:85 platform/gui.cpp:90 solvespace.cpp:583 +msgctxt "file-type" +msgid "SolveSpace models" +msgstr "Проекты SolveSpace" + +#: platform/gui.cpp:89 +msgctxt "file-type" +msgid "ALL" +msgstr "ВСЕ" + +#: platform/gui.cpp:91 +msgctxt "file-type" +msgid "IDF circuit board" +msgstr "IDF печатная плата" + +#: platform/gui.cpp:92 +msgctxt "file-type" +msgid "STL triangle mesh" +msgstr "STL треугольная сетка" + +#: platform/gui.cpp:96 +msgctxt "file-type" +msgid "PNG image" +msgstr "PNG изображение" + +#: platform/gui.cpp:100 +msgctxt "file-type" +msgid "STL mesh" +msgstr "STL полигональная сетка" + +#: platform/gui.cpp:101 +msgctxt "file-type" +msgid "Wavefront OBJ mesh" +msgstr "Wavefront OBJ полигональная сетка" + +#: platform/gui.cpp:102 +msgctxt "file-type" +msgid "Three.js-compatible mesh, with viewer" +msgstr "Three.js-совместимая полигональная сетка с просмотрщиком" + +#: platform/gui.cpp:103 +msgctxt "file-type" +msgid "Three.js-compatible mesh, mesh only" +msgstr "Three.js-совместимая полигональная сетка" + +#: platform/gui.cpp:104 +msgctxt "file-type" +msgid "VRML text file" +msgstr "VRML файл" + +#: platform/gui.cpp:108 platform/gui.cpp:115 platform/gui.cpp:122 +msgctxt "file-type" +msgid "STEP file" +msgstr "STEP файл" + +#: platform/gui.cpp:112 +msgctxt "file-type" +msgid "PDF file" +msgstr "PDF документ" + +#: platform/gui.cpp:113 +msgctxt "file-type" +msgid "Encapsulated PostScript" +msgstr "Encapsulated PostScript" + +#: platform/gui.cpp:114 +msgctxt "file-type" +msgid "Scalable Vector Graphics" +msgstr "SVG изображение" + +#: platform/gui.cpp:116 platform/gui.cpp:123 +msgctxt "file-type" +msgid "DXF file (AutoCAD 2007)" +msgstr "DXF файл (AutoCAD 2007)" + +#: platform/gui.cpp:117 +msgctxt "file-type" +msgid "HPGL file" +msgstr "HPGL файл" + +#: platform/gui.cpp:118 +msgctxt "file-type" +msgid "G Code" +msgstr "G Code" + +#: platform/gui.cpp:127 +msgctxt "file-type" +msgid "AutoCAD DXF and DWG files" +msgstr "AutoCAD DXF и DWG файлы" + +#: platform/gui.cpp:131 +msgctxt "file-type" +msgid "Comma-separated values" +msgstr "CSV файлы (значения, разделенные запятой)" + +#: platform/guigtk.cpp:1434 platform/guimac.mm:1513 platform/guiwin.cpp:1641 +msgid "untitled" +msgstr "без имени" + +#: platform/guigtk.cpp:1445 platform/guigtk.cpp:1481 platform/guimac.mm:1471 +#: platform/guiwin.cpp:1639 +msgctxt "title" +msgid "Save File" +msgstr "Сохранить файл" + +#: platform/guigtk.cpp:1446 platform/guigtk.cpp:1482 platform/guimac.mm:1454 +#: platform/guiwin.cpp:1645 +msgctxt "title" +msgid "Open File" +msgstr "Открыть файл" + +#: platform/guigtk.cpp:1449 platform/guigtk.cpp:1488 +msgctxt "button" +msgid "_Cancel" +msgstr "О_тменить" + +#: platform/guigtk.cpp:1450 platform/guigtk.cpp:1486 +msgctxt "button" +msgid "_Save" +msgstr "_Сохранить" + +#: platform/guigtk.cpp:1451 platform/guigtk.cpp:1487 +msgctxt "button" +msgid "_Open" +msgstr "_Открыть" + +#: solvespace.cpp:175 +msgctxt "title" +msgid "Autosave Available" +msgstr "Доступно автосохранение" + +#: solvespace.cpp:176 +msgctxt "dialog" +msgid "An autosave file is available for this sketch." +msgstr "Автоматически сохраненный файл доступен для данного проекта." + +#: solvespace.cpp:177 +msgctxt "dialog" +msgid "Do you want to load the autosave file instead?" +msgstr "Хотите загрузить автосохраненный файл вместо исходного?" + +#: solvespace.cpp:178 +msgctxt "button" +msgid "&Load autosave" +msgstr "&Загрузить автосохранение" + +#: solvespace.cpp:180 +msgctxt "button" +msgid "Do&n't Load" +msgstr "&Не загружать" + +#: solvespace.cpp:640 +msgctxt "title" +msgid "Modified File" +msgstr "Измененный файл" + +#: solvespace.cpp:642 +#, c-format +msgctxt "dialog" +msgid "Do you want to save the changes you made to the sketch “%s”?" +msgstr "Сохранить изменения, сделанные в файле “%s”?" + +#: solvespace.cpp:645 +msgctxt "dialog" +msgid "Do you want to save the changes you made to the new sketch?" +msgstr "Сохранить изменения, сделанные в новом проекте?" + +#: solvespace.cpp:648 +msgctxt "dialog" +msgid "Your changes will be lost if you don't save them." +msgstr "Изменения будут утеряны, если их не сохранить." + +#: solvespace.cpp:649 +msgctxt "button" +msgid "&Save" +msgstr "&Сохранить" + +#: solvespace.cpp:651 +msgctxt "button" +msgid "Do&n't Save" +msgstr "&Не сохранять" + +#: solvespace.cpp:672 +msgctxt "title" +msgid "(new sketch)" +msgstr "(новый проект)" + +#: solvespace.cpp:683 +msgctxt "title" +msgid "Property Browser" +msgstr "Браузер" + +#: solvespace.cpp:746 +msgid "" +"Constraints are currently shown, and will be exported in the toolpath. This " +"is probably not what you want; hide them by clicking the link at the top of " +"the text window." +msgstr "" +"Ограничения отображаются, и будут экспортированы в файл. Предположительно, " +"это не то, что требуется, если так, необходимо спрятать их, нажав ссылку " +"вверху окна Браузера." + +#: solvespace.cpp:834 +#, c-format +msgid "" +"Can't identify file type from file extension of filename '%s'; try .dxf or ." +"dwg." +msgstr "" +"Неподдерживаемый тип файла '%s'. Поддерживаются файлы с расширением .dxf и ." +"dwg." + +#: solvespace.cpp:886 +msgid "Constraint must have a label, and must not be a reference dimension." +msgstr "У ограничения должно быть значение и оно не должно быть справочным." + +#: solvespace.cpp:890 +msgid "Bad selection for step dimension; select a constraint." +msgstr "" +"Неправильное выделение для операции изменения значения с заданным шагом; " +"необходимо выбрать ограничение со значением." + +#: solvespace.cpp:914 +msgid "The assembly does not interfere, good." +msgstr "Сборка не содержит пересечения деталей - это хорошо." + +#: solvespace.cpp:930 +#, c-format +msgid "" +"The volume of the solid model is:\n" +"\n" +" %s" +msgstr "" +"Объем тел:\n" +"\n" +" %s" + +#: solvespace.cpp:939 +#, c-format +msgid "" +"\n" +"The volume of current group mesh is:\n" +"\n" +" %s" +msgstr "" +"\n" +"Объем тел текущей группы:\n" +"\n" +" %s" + +#: solvespace.cpp:944 +msgid "" +"\n" +"\n" +"Curved surfaces have been approximated as triangles.\n" +"This introduces error, typically of around 1%." +msgstr "" +"\n" +"\n" +"Кривые аппроксимированы кусочно-линейными функциями.\n" +"Это приводит к ошибке в расчетах, обычно в пределах 1%." + +#: solvespace.cpp:959 +#, c-format +msgid "" +"The surface area of the selected faces is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"Площадь поверхности выбранных граней:\n" +"\n" +" %s\n" +"\n" +"Кривые аппроксимированы кусочно-линейными функциями.\n" +"Это приводит к ошибке в расчетах, обычно в пределах 1%%." + +#: solvespace.cpp:968 +msgid "" +"This group does not contain a correctly-formed 2d closed area. It is open, " +"not coplanar, or self-intersecting." +msgstr "" +"Эта группа не содержит замкнутых областей. В ней нет замкнутых контуров, " +"примитивы не лежат в одной плоскости или самопересекаются." + +#: solvespace.cpp:980 +#, c-format +msgid "" +"The area of the region sketched in this group is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"Площадь замкнутой области текущей группы:\n" +"\n" +" %s\n" +"\n" +"Кривые аппроксимированы кусочно-линейными функциями.\n" +"Это приводит к ошибке в расчетах, обычно в пределах 1%%." + +#: solvespace.cpp:1000 +#, c-format +msgid "" +"The total length of the selected entities is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"Длина выбранных примитивов:\n" +"\n" +" %s\n" +"\n" +"Кривые аппроксимированы кусочно-линейными функциями.\n" +"Это приводит к ошибке в расчетах, обычно в пределах 1%%." + +#: solvespace.cpp:1006 +msgid "Bad selection for perimeter; select line segments, arcs, and curves." +msgstr "" +"Неправильное выделение для расчета периметра; необходимо выбирать только " +"отрезки, дуги и кривые." + +#: solvespace.cpp:1022 +msgid "Bad selection for trace; select a single point." +msgstr "Неправильное выделение для трассировки; необходимо выбрать одну точку." + +#: solvespace.cpp:1049 +#, c-format +msgid "Couldn't write to '%s'" +msgstr "Невозможно записать в '%s'" + +#: solvespace.cpp:1079 +msgid "The mesh is self-intersecting (NOT okay, invalid)." +msgstr "Полигональная сетка содержит самопересечения (это плохо)" + +#: solvespace.cpp:1080 +msgid "The mesh is not self-intersecting (okay, valid)." +msgstr "Полигональная сетка не содержит самопересечений (это хорошо)" + +#: solvespace.cpp:1082 +msgid "The mesh has naked edges (NOT okay, invalid)." +msgstr "Полигональная сетка содержит \"оголенные\" ребра (это плохо)" + +#: solvespace.cpp:1083 +msgid "The mesh is watertight (okay, valid)." +msgstr "Полигональная сетка герметична (это хорошо)" + +#: solvespace.cpp:1086 +#, c-format +msgid "" +"\n" +"\n" +"The model contains %d triangles, from %d surfaces." +msgstr "" +"\n" +"\n" +"Модель содержит %d треугольников, содержащихся в %d поверхностях." + +#: solvespace.cpp:1090 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"Zero problematic edges, good.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"Нет проблемных ребер - это хорошо.%s" + +#: solvespace.cpp:1093 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d problematic edges, bad.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d найдены проблемные ребра - это плохо.%s" + +#: solvespace.cpp:1106 +#, c-format +msgid "" +"This is SolveSpace version %s.\n" +"\n" +"For more information, see http://solvespace.com/\n" +"\n" +"SolveSpace is free software: you are free to modify\n" +"and/or redistribute it under the terms of the GNU\n" +"General Public License (GPL) version 3 or later.\n" +"\n" +"There is NO WARRANTY, to the extent permitted by\n" +"law. For details, visit http://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues and other authors.\n" +msgstr "" +"Это SolveSpace версии %s.\n" +"\n" +"Для дополнительной информации посетите сайт: http://solvespace.com/\n" +"\n" +"SolveSpace — свободное программное обеспечение: поощряется изменение, " +"улучшение\n" +"распространение программы по условиям лицензии GNU\n" +"General Public License (GPL) версии 3 или новее.\n" +"\n" +"НИКАКИХ ГАРАНТИЙ за пределами прав, \n" +"предусмотренных законом. Дополнительная информация на сайте: http://gnu.org/" +"licenses/\n" +"\n" +"© 2008-%d Джонатан Уэстхьюс и другие авторы.\n" + +#: style.cpp:185 +msgid "" +"Can't assign style to an entity that's derived from another entity; try " +"assigning a style to this entity's parent." +msgstr "" +"Невозможно применить стиль к примитиву, который произошел от другого " +"примитива. Попробуйте применить стиль к исходному примитиву." + +#: style.cpp:735 +msgid "Style name cannot be empty" +msgstr "Имя стиля не может быть пустым" + +#: textscreens.cpp:837 +msgid "Can't repeat fewer than 1 time." +msgstr "Невозможно сделать повторение меньше, чем 1 раз." + +#: textscreens.cpp:841 +msgid "Can't repeat more than 999 times." +msgstr "Невозможно сделать повтор больше, чем 999 раз." + +#: textscreens.cpp:866 +msgid "Group name cannot be empty" +msgstr "Имя группы не может быть пустым" + +#: textscreens.cpp:918 +msgid "Opacity must be between zero and one." +msgstr "Прозрачность должна быть числом от нуля до единицы." + +#: textscreens.cpp:953 +msgid "Radius cannot be zero or negative." +msgstr "Радиус не может быть нулевым или отрицательным." + +#: toolbar.cpp:18 +msgid "Sketch line segment" +msgstr "Начертить отрезок прямой" + +#: toolbar.cpp:20 +msgid "Sketch rectangle" +msgstr "Начертить прямоугольник" + +#: toolbar.cpp:22 +msgid "Sketch circle" +msgstr "Начертить окружность" + +#: toolbar.cpp:24 +msgid "Sketch arc of a circle" +msgstr "Начертить дугу окружности" + +#: toolbar.cpp:26 +msgid "Sketch curves from text in a TrueType font" +msgstr "Начертить текст TrueType" + +#: toolbar.cpp:28 +msgid "Sketch image from a file" +msgstr "Импортировать изображение из файла" + +#: toolbar.cpp:30 +msgid "Create tangent arc at selected point" +msgstr "Создать скругление в выбранной точке" + +#: toolbar.cpp:32 +msgid "Sketch cubic Bezier spline" +msgstr "Начертить кубический сплайн Безье" + +#: toolbar.cpp:34 +msgid "Sketch datum point" +msgstr "Начертить опорную точку" + +#: toolbar.cpp:36 +msgid "Toggle construction" +msgstr "Переключить вспомогательную геометрию" + +#: toolbar.cpp:38 +msgid "Split lines / curves where they intersect" +msgstr "Разбить кривые по пересечению" + +#: toolbar.cpp:42 +msgid "Constrain distance / diameter / length" +msgstr "Ограничение расстояния / диаметра / длины" + +#: toolbar.cpp:44 +msgid "Constrain angle" +msgstr "Ограничение угла" + +#: toolbar.cpp:46 +msgid "Constrain to be horizontal" +msgstr "Ограничение горизонтальности" + +#: toolbar.cpp:48 +msgid "Constrain to be vertical" +msgstr "Ограничение вертикальности" + +#: toolbar.cpp:50 +msgid "Constrain to be parallel or tangent" +msgstr "Ограничение параллельности / касательности" + +#: toolbar.cpp:52 +msgid "Constrain to be perpendicular" +msgstr "Ограничение перпендикулярности" + +#: toolbar.cpp:54 +msgid "Constrain point on line / curve / plane / point" +msgstr "Ограничение точка на точке / линии / окружности / дуге / плоскости" + +#: toolbar.cpp:56 +msgid "Constrain symmetric" +msgstr "Симметричность" + +#: toolbar.cpp:58 +msgid "Constrain equal length / radius / angle" +msgstr "Ограничение равенства длин / радиусов / углов" + +#: toolbar.cpp:60 +msgid "Constrain normals in same orientation" +msgstr "Ограничение идентичности ориентации координатных базисов" + +#: toolbar.cpp:62 +msgid "Other supplementary angle" +msgstr "Переключить смежный угол" + +#: toolbar.cpp:64 +msgid "Toggle reference dimension" +msgstr "Переключить режим размера для справок" + +#: toolbar.cpp:68 +msgid "New group extruding active sketch" +msgstr "Создать группу выдавливания текущего эскиза" + +#: toolbar.cpp:70 +msgid "New group rotating active sketch" +msgstr "Создать группу тела вращения текущего эскиза" + +#: toolbar.cpp:72 +msgid "New group helix from active sketch" +msgstr "Создать группу тела выдавливания по винтовой линии из текущего эскиза" + +#: toolbar.cpp:74 +msgid "New group revolve active sketch" +msgstr "Создать группу тела вращения на угол из текущего эскиза" + +#: toolbar.cpp:76 +msgid "New group step and repeat rotating" +msgstr "Создать группу кругового массива" + +#: toolbar.cpp:78 +msgid "New group step and repeat translating" +msgstr "Создать группу линейного массива" + +#: toolbar.cpp:80 +msgid "New group in new workplane (thru given entities)" +msgstr "Создать группу в новой рабочей плоскости (через выбранные примитивы)" + +#: toolbar.cpp:82 +msgid "New group in 3d" +msgstr "Новая группа в 3d" + +#: toolbar.cpp:84 +msgid "New group linking / assembling file" +msgstr "Новая группа импорта детали / сборки" + +#: toolbar.cpp:88 +msgid "Nearest isometric view" +msgstr "Ближайший изометрический вид" + +#: toolbar.cpp:90 +msgid "Align view to active workplane" +msgstr "Выровнять вид по рабочей плоскости" + +#: util.cpp:165 +msgctxt "title" +msgid "Error" +msgstr "Ошибка" + +#: util.cpp:165 +msgctxt "title" +msgid "Message" +msgstr "Сообщение" + +#: util.cpp:170 +msgctxt "button" +msgid "&OK" +msgstr "&OK" + +#: view.cpp:127 +msgid "Scale cannot be zero or negative." +msgstr "Масштабный коэффициент не может быть нулевым или отрицательным." + +#: view.cpp:139 view.cpp:148 +msgid "Bad format: specify x, y, z" +msgstr "Неверный формат: введите данные как x, y, z" + +#~ msgid "" +#~ "Bad selection for on point / curve / plane constraint. This constraint " +#~ "can apply to:\n" +#~ "\n" +#~ " * two points (points coincident)\n" +#~ " * a point and a workplane (point in plane)\n" +#~ " * a point and a line segment (point on line)\n" +#~ " * a point and a circle or arc (point on curve)\n" +#~ " * a point and a plane face (point on face)\n" +#~ msgstr "" +#~ "Неправильное выделение для ограничения 'точка на примитиве'.\n" +#~ "Ограничение может принимать в качестве выделения следующие примитивы:\n" +#~ "\n" +#~ " * две точки (совпадение точек)\n" +#~ " * точку и рабочую плоскость (точка в плоскости)\n" +#~ " * точку и отрезок (точка на линии)\n" +#~ " * точку и окружность / дугу / сплайн (точка на кривой)\n" +#~ " * точку и грань (точка на грани)\n" + +#~ msgid "" +#~ "Bad selection for equal length / radius constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two line segments (equal length)\n" +#~ " * two line segments and two points (equal point-line distances)\n" +#~ " * a line segment and two points (equal point-line distances)\n" +#~ " * a line segment, and a point and line segment (point-line distance " +#~ "equals length)\n" +#~ " * four line segments or normals (equal angle between A,B and C,D)\n" +#~ " * three line segments or normals (equal angle between A,B and B,C)\n" +#~ " * two circles or arcs (equal radius)\n" +#~ " * a line segment and an arc (line segment length equals arc length)\n" +#~ msgstr "" +#~ "Неправильное выделение для ограничения 'равенство примитивов'.\n" +#~ "Ограничение может принимать в качестве выделения следующие примитивы:\n" +#~ "\n" +#~ " * два отрезка (равенство длин отрезков)\n" +#~ " * два отрезка и две точки (равенство расстояний от точек до линий)\n" +#~ " * отрезок и две точки (равенство расстояний от точек до линии)\n" +#~ " * отрезок, точку и отрезок (равенство длины отрезка расстоянию от " +#~ "точки до линии)\n" +#~ " * четыре отрезка или нормали (равенство углов A,B и C,D)\n" +#~ " * три отрезка или нормали (равенство углов A,B and B,C)\n" +#~ " * две окружности / дуги (равенство радиусов)\n" +#~ " * отрезок и дугу (равенство длины отрезка и длины дуги)\n" + +#~ msgid "" +#~ "Bad selection for horizontal / vertical constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two points\n" +#~ " * a line segment\n" +#~ msgstr "" +#~ "Неправильное выделение для ограничения 'горизонтальность / " +#~ "вертикальность'.\n" +#~ "Ограничение может принимать в качестве выделения следующие примитивы:\n" +#~ "\n" +#~ " * две точки\n" +#~ " * отрезок\n" + +#~ msgid "" +#~ "Bad selection for angle constraint. This constraint can apply to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ " * a line segment and a normal\n" +#~ " * two normals\n" +#~ msgstr "" +#~ "Неправильное выделение для ограничения углового размера.\n" +#~ "Ограничение может принимать в качестве выделения следующие примитивы:\n" +#~ "\n" +#~ " * два отрезка\n" +#~ " * отрезок и координатный базис (нормаль)\n" +#~ " * два координатных базиса (нормали)\n" + +#~ msgid "" +#~ "Bad selection for parallel / tangent constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two line segments (parallel)\n" +#~ " * a line segment and a normal (parallel)\n" +#~ " * two normals (parallel)\n" +#~ " * two line segments, arcs, or beziers, that share an endpoint " +#~ "(tangent)\n" +#~ msgstr "" +#~ "Неправильное выделение для ограничения параллельности / касательности.\n" +#~ "Ограничение может принимать в качестве выделения следующие примитивы:\n" +#~ "\n" +#~ " * два отрезка (параллельность)\n" +#~ " * отрезок и координатный базис (нормаль) (параллельность)\n" +#~ " * два координатных базиса (нормали) (параллельность)\n" +#~ " * два отрезка, две дуги или два сплайна, соединенных крайними точками " +#~ "(касательность)\n" + +#~ msgid "" +#~ "Bad selection for perpendicular constraint. This constraint can apply " +#~ "to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ " * a line segment and a normal\n" +#~ " * two normals\n" +#~ msgstr "" +#~ "Неправильное выделение для ограничения перпендикулярности.\n" +#~ "Ограничение может принимать в качестве выделения следующие примитивы:\n" +#~ "\n" +#~ " * два отрезка\n" +#~ " * отрезок и координатный базис (нормаль)\n" +#~ " * два координатных базиса (нормали)\n" + +#~ msgid "A&ngle" +#~ msgstr "&Угол" + +#~ msgid "E&qual Length / Radius / Angle" +#~ msgstr "&Равенство Длин / Радиусов / Углов" + +#~ msgid "" +#~ "Bad selection for length ratio constraint. This constraint can apply to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ msgstr "" +#~ "Неправильное выделение для ограничения 'отношение длин'.\n" +#~ "Ограничение может принимать в качестве выделения следующие примитивы:\n" +#~ "\n" +#~ " * два отрезка\n" + +#~ msgid "" +#~ "Bad selection for length difference constraint. This constraint can apply " +#~ "to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ msgstr "" +#~ "Неправильное выделение для ограничения 'разница длин'.\n" +#~ "Ограничение может принимать в качестве выделения следующие примитивы:\n" +#~ "\n" +#~ " * два отрезка\n" + +#~ msgid "Length Ra&tio" +#~ msgstr "Отно&шение Длин" + +#~ msgid "Length Diff&erence" +#~ msgstr "Ра&зница Длин" + +#~ msgid "" +#~ "Bad selection for new sketch in workplane. This group can be created " +#~ "with:\n" +#~ "\n" +#~ " * a point (through the point, orthogonal to coordinate axes)\n" +#~ " * a point and two line segments (through the point, parallel to the " +#~ "lines)\n" +#~ " * a workplane (copy of the workplane)\n" +#~ msgstr "" +#~ "Неправильное выделение для создания эскиза.\n" +#~ "Группа может быть создана, используя в качестве выделения следующие " +#~ "примитивы:\n" +#~ "\n" +#~ " * точку (рабочая плоскость, ориентированная к ближайшему виду, " +#~ "проходящая через точку)\n" +#~ " * точку и два отрезка (рабочая плоскость, проходящая через точку и " +#~ "параллельная отрезкам)\n" +#~ " * рабочую плоскость (копия рабочей плоскости)\n" + +#~ msgid "Specify between 0 and 8 digits after the decimal." +#~ msgstr "Введите число от 0 до 8." + +#~ msgid "Show Degrees of &Freedom" +#~ msgstr "Показать Степени С&вободы" + +#~ msgid "click to place bottom left of text" +#~ msgstr "кликните мышью, чтобы расположить текст" + +#~ msgid "Do you want to save the changes you made to the new sketch?" +#~ msgstr "Хотите сохранить ваши изменения?" + +#~ msgid "Your changes will be lost if you don't save them." +#~ msgstr "Ваши изменения будут утеряны, если вы их не сохраните." + +#~ msgctxt "button" +#~ msgid "Save" +#~ msgstr "Сохранить" + +#~ msgctxt "button" +#~ msgid "Cancel" +#~ msgstr "Отменить" + +#~ msgctxt "button" +#~ msgid "Don't Save" +#~ msgstr "Не Сохранять" + +#~ msgid "An autosave file is available for this project." +#~ msgstr "Файлы автосохранения доступны для данного проекта." + +#~ msgid "Do you want to load the autosave file instead?" +#~ msgstr "Хотите загрузить их вместо открываемого файла?" + +#~ msgctxt "button" +#~ msgid "Load" +#~ msgstr "Загрузить Автосохранение" + +#~ msgctxt "button" +#~ msgid "Don't Load" +#~ msgstr "Просто Открыть Файл" + +#~ msgid "" +#~ "Do you want to locate it manually?\n" +#~ "If you select “No”, any geometry that depends on the missing file will be " +#~ "removed." +#~ msgstr "" +#~ "Хотите найти их вручную?\n" +#~ "Если вы ответите \"Нет\", то вся геометрия, которая зависит от " +#~ "отсутствующего файла будет удалена." + +#~ msgctxt "button" +#~ msgid "Yes" +#~ msgstr "Да" + +#~ msgctxt "button" +#~ msgid "No" +#~ msgstr "Нет" + +#~ msgctxt "button" +#~ msgid "OK" +#~ msgstr "ХОРОШО" + +#~ msgid "_Cancel" +#~ msgstr "Отменить" + +#~ msgid "_Open" +#~ msgstr "Открыть" + +#~ msgid "" +#~ "The file has changed since it was last saved.\n" +#~ "\n" +#~ "Do you want to save the changes?" +#~ msgstr "" +#~ "Файл имеет несохраненные изменения.\n" +#~ "\n" +#~ "Хотите сохранить их?" + +#~ msgctxt "button" +#~ msgid "Do_n't Save" +#~ msgstr "Не Сохранять" + +#~ msgid "" +#~ "An autosave file is available for this project.\n" +#~ "\n" +#~ "Do you want to load the autosave file instead?" +#~ msgstr "" +#~ "Файлы автосохранения доступны для данного проекта.\n" +#~ "\n" +#~ "Хотите загрузить их вместо открываемого файла?" + +#~ msgctxt "button" +#~ msgid "_Load autosave" +#~ msgstr "Загрузить Автосохранение" + +#~ msgctxt "button" +#~ msgid "Do_n't Load" +#~ msgstr "Не Загружать" + +#~ msgctxt "button" +#~ msgid "_Yes" +#~ msgstr "Да" + +#~ msgctxt "button" +#~ msgid "_No" +#~ msgstr "Нет" + +#~ msgid "" +#~ "Select two entities that intersect each other (e.g. two lines or two " +#~ "circles or a circle and a line)." +#~ msgstr "" +#~ "Выберите два пересекающихся примитива. Например, две линии или две " +#~ "окружности или линию и окружность." + +#~ msgid "Show Menu &Bar" +#~ msgstr "Показывать Мен&ю" + +#~ msgctxt "group-name" +#~ msgid "link" +#~ msgstr "ссылка-на-деталь" + +#~ msgid "Scale must not be zero or negative!" +#~ msgstr "" +#~ "Коэффициент масштабирования не может быть нулевым или отрицательным!" diff --git a/res/locales/tr_TR.po b/res/locales/tr_TR.po new file mode 100644 index 0000000..097e14a --- /dev/null +++ b/res/locales/tr_TR.po @@ -0,0 +1,2330 @@ +# Turkish translations for SolveSpace package. +# Copyright (C) 2017 the SolveSpace authors +# This file is distributed under the same license as the SolveSpace package. +# SPDX-FileCopyrightText: 2022, 2025 Mustafa Halil GÖRENTAŞ +# +msgid "" +msgstr "" +"Project-Id-Version: SolveSpace 3.1\n" +"Report-Msgid-Bugs-To: phkahler@gmail.com\n" +"POT-Creation-Date: 2025-01-26 21:04+0200\n" +"PO-Revision-Date: 2025-01-29 21:16+0300\n" +"Last-Translator: mustafa halil \n" +"Language-Team: Turkish \n" +"Language: tr_TR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 23.08.5\n" + +#: clipboard.cpp:314 +#, fuzzy +msgid "" +"Cut, paste, and copy work only in a workplane.\n" +"\n" +"Activate one with Sketch -> In Workplane." +msgstr "" +"Çalışmayı yalnızca bir çalışma düzleminde kesin, yapıştırın ve kopyalayın.\n" +"\n" +"Çizim -> Çalışma Düzleminde menüsü ile bir düzlemi etkinleştirin." + +#: clipboard.cpp:331 +msgid "Clipboard is empty; nothing to paste." +msgstr "Pano boş; yapıştırılacak bir şey yok." + +#: clipboard.cpp:378 +msgid "Number of copies to paste must be at least one." +msgstr "Yapıştırılacak kopya sayısı en az bir olmalıdır." + +#: clipboard.cpp:394 textscreens.cpp:879 +msgid "Scale cannot be zero." +msgstr "Ölçek sıfır olamaz." + +#: clipboard.cpp:436 +msgid "Select one point to define origin of rotation." +msgstr "Çevirme merkezini tanımlamak için bir nokta seçin." + +#: clipboard.cpp:448 +msgid "Select two points to define translation vector." +msgstr "Doğrusal kopyalama vektörünü tanımlamak için iki nokta seçin." + +#: clipboard.cpp:458 +msgid "" +"Transformation is identity. So all copies will be exactly on top of each " +"other." +msgstr "Dönüşüm özdeştir. Yani tüm kopyalar tam olarak üst üste gelecek." + +#: clipboard.cpp:462 +msgid "Too many items to paste; split this into smaller pastes." +msgstr "" +"Yapıştırılamayacak kadar çok öğe; bunu daha küçük yapıştıma şeklinde bölün." + +#: clipboard.cpp:467 +msgid "No workplane active." +msgstr "Etkin çalışma düzlemi yok." + +#: confscreen.cpp:410 +msgid "Bad format: specify coordinates as x, y, z" +msgstr "Hatalı biçim: koordinatları x, y, z olarak belirtin" + +#: confscreen.cpp:420 style.cpp:729 textscreens.cpp:910 +msgid "Bad format: specify color as r, g, b" +msgstr "Hatalı biçim: rengi r, g, b olarak belirtin" + +#: confscreen.cpp:446 +msgid "" +"The perspective factor will have no effect until you enable View -> Use " +"Perspective Projection." +msgstr "" +"Görünüm -> Perspektif Projeksiyonu Kullan'ı etkinleştirene kadar perspektif " +"katsayısının hiçbir etkisi olmayacaktır." + +#: confscreen.cpp:464 confscreen.cpp:474 +#, c-format +msgid "Specify between 0 and %d digits after the decimal." +msgstr "Ondalık basamak sonrası 0 ile %d arasında rakam belirtin." + +#: confscreen.cpp:486 +msgid "Export scale must not be zero!" +msgstr "Dışa aktarma ölçeği sıfır olmamalıdır!" + +#: confscreen.cpp:498 +msgid "Cutter radius offset must not be negative!" +msgstr "Kesici yarıçap ofseti negatif olmamalıdır!" + +#: confscreen.cpp:557 +msgid "Bad value: autosave interval should be positive" +msgstr "Hatalı değer: otomatik kaydetme süresi pozitif olmalıdır" + +#: confscreen.cpp:560 +msgid "Bad format: specify interval in integral minutes" +msgstr "Hatalı biçim: süre aralığını dakika cinsinden belirtin" + +#: constraint.cpp:12 +msgctxt "constr-name" +msgid "pts-coincident" +msgstr "çakışık-nktlar" + +#: constraint.cpp:13 +msgctxt "constr-name" +msgid "pt-pt-distance" +msgstr "nkt-nkt-mesafe" + +#: constraint.cpp:14 +msgctxt "constr-name" +msgid "pt-line-distance" +msgstr "nkt-çizgi-mesafesi" + +#: constraint.cpp:15 +msgctxt "constr-name" +msgid "pt-plane-distance" +msgstr "nkt-düzlem-mesafesi" + +#: constraint.cpp:16 +msgctxt "constr-name" +msgid "pt-face-distance" +msgstr "nkt-yüzey-mesafesi" + +#: constraint.cpp:17 +msgctxt "constr-name" +msgid "proj-pt-pt-distance" +msgstr "proj-nkt-nkt-mesafesi" + +#: constraint.cpp:18 +msgctxt "constr-name" +msgid "pt-in-plane" +msgstr "düzlemde-nkt" + +#: constraint.cpp:19 +msgctxt "constr-name" +msgid "pt-on-line" +msgstr "çizgide-nkt" + +#: constraint.cpp:20 +msgctxt "constr-name" +msgid "pt-on-face" +msgstr "yüzeyde-nkt" + +#: constraint.cpp:21 +msgctxt "constr-name" +msgid "eq-length" +msgstr "eşit-uzunluk" + +#: constraint.cpp:22 +msgctxt "constr-name" +msgid "eq-length-and-pt-ln-dist" +msgstr "eşit-uzunluk-ve-çzg-nkt-mesafesi" + +#: constraint.cpp:23 +msgctxt "constr-name" +msgid "eq-pt-line-distances" +msgstr "eşit-nkt-çizgi-mesafesi" + +#: constraint.cpp:24 +msgctxt "constr-name" +msgid "length-ratio" +msgstr "uzunluk-oranı" + +#: constraint.cpp:25 +msgctxt "constr-name" +msgid "arc-arc-length-ratio" +msgstr "yay-yay-uzunluk-oranı" + +#: constraint.cpp:26 +msgctxt "constr-name" +msgid "arc-line-length-ratio" +msgstr "yay-çizgi-uzunluk-oranı" + +#: constraint.cpp:27 +msgctxt "constr-name" +msgid "length-difference" +msgstr "uzunluk-farkı" + +#: constraint.cpp:28 +msgctxt "constr-name" +msgid "arc-arc-len-difference" +msgstr "yay-yay-uzunluk-farkı" + +#: constraint.cpp:29 +msgctxt "constr-name" +msgid "arc-line-len-difference" +msgstr "yay-çizgi-uzunluk-farkı" + +#: constraint.cpp:30 +msgctxt "constr-name" +msgid "symmetric" +msgstr "simetrik" + +#: constraint.cpp:31 +msgctxt "constr-name" +msgid "symmetric-h" +msgstr "simetrik-y" + +#: constraint.cpp:32 +msgctxt "constr-name" +msgid "symmetric-v" +msgstr "simetrik-d" + +#: constraint.cpp:33 +msgctxt "constr-name" +msgid "symmetric-line" +msgstr "simetrik-çizgi" + +#: constraint.cpp:34 +msgctxt "constr-name" +msgid "at-midpoint" +msgstr "orta noktada" + +#: constraint.cpp:35 +msgctxt "constr-name" +msgid "horizontal" +msgstr "yatay" + +#: constraint.cpp:36 +msgctxt "constr-name" +msgid "vertical" +msgstr "dikey" + +#: constraint.cpp:37 +msgctxt "constr-name" +msgid "diameter" +msgstr "çap" + +#: constraint.cpp:38 +msgctxt "constr-name" +msgid "pt-on-circle" +msgstr "çemberde-nkt" + +#: constraint.cpp:39 +msgctxt "constr-name" +msgid "same-orientation" +msgstr "aynı-yön" + +#: constraint.cpp:40 +msgctxt "constr-name" +msgid "angle" +msgstr "açı" + +#: constraint.cpp:41 +msgctxt "constr-name" +msgid "parallel" +msgstr "paralel" + +#: constraint.cpp:42 +msgctxt "constr-name" +msgid "arc-line-tangent" +msgstr "yay-çizgi-teğet" + +#: constraint.cpp:43 +msgctxt "constr-name" +msgid "cubic-line-tangent" +msgstr "k.eğri-çizgi-teğet" + +#: constraint.cpp:44 +msgctxt "constr-name" +msgid "curve-curve-tangent" +msgstr "eğri-eğri-teğet" + +#: constraint.cpp:45 +msgctxt "constr-name" +msgid "perpendicular" +msgstr "dik" + +#: constraint.cpp:46 +msgctxt "constr-name" +msgid "eq-radius" +msgstr "eşit-yarıçap" + +#: constraint.cpp:47 +msgctxt "constr-name" +msgid "eq-angle" +msgstr "eşit-açı" + +#: constraint.cpp:48 +msgctxt "constr-name" +msgid "eq-line-len-arc-len" +msgstr "eşit-çizgi-uzn-yay-uzn" + +#: constraint.cpp:49 +msgctxt "constr-name" +msgid "lock-where-dragged" +msgstr "sürüklendiği-yerde-kilitli" + +#: constraint.cpp:50 +msgctxt "constr-name" +msgid "comment" +msgstr "yorum" + +#: constraint.cpp:151 +msgid "" +"The point you selected does not belong to the arc. The arc and line segment " +"do not share an end point.\n" +"\n" +"Select the end point of the arc at which you want it to be tangent to the " +"line." +msgstr "" +"Seçtiğiniz nokta yaya ait değildir. Yay ve doğru parçası bir bitiş " +"noktasını paylaşmaz.\n" +"\n" +"Çizgiye teğet olmasını istediğiniz yayın bitiş noktasını seçin." + +#: constraint.cpp:158 +msgid "" +"The tangent arc and line segment must share an endpoint. Constrain them with " +"Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the arc at which you want it to be " +"tangent to the line." +msgstr "" +"Teğet için yay ve çizgi parçası bir uç noktayı paylaşmalıdır. Teğet " +"kısıtlamasından önce bunları Kısıtla -> Noktada ile kısıtlayın.\n" +"\n" +"Alternatif olarak, çizgiye teğet olmasını istediğiniz yayın uç " +"noktasını seçin." + +#: constraint.cpp:186 +msgid "" +"The point you selected is not an end point of the cubic spline. The spline " +"and line segment do not share an end point.\n" +"\n" +"Select the end point of the spline at which you want it to be tangent to the " +"line." +msgstr "" +"Seçtiğiniz nokta kübik eğrinin bir uç noktası değildir. Eğri ve doğru " +"parçası bir bitiş noktasını paylaşmaz.\n" +"\n" +"Çizgiye teğet olmasını istediğiniz eğrinin uç noktasını seçin." + +#: constraint.cpp:193 +msgid "" +"The tangent cubic spline and line segment must share an endpoint. Constrain " +"them with Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the cubic spline at which you want it " +"to be tangent to the line." +msgstr "" +"Teğet için kübik eğri ve çizgi parçası bir uç noktayı paylaşmalıdır. Teğet " +"kısıtlamasından önce onları Kısıtla -> Noktada ile kısıtlayın.\n" +"\n" +"Alternatif olarak, kübik eğrinin çizgiye teğet olmasını istediğiniz uç " +"noktasını seçin." + +#: constraint.cpp:237 +msgid "" +"The points you selected are not end points of the two curves. The curves do " +"not share an end point.\n" +"\n" +"Select the end points of both curves at which you want them to be tangent to " +"each other." +msgstr "" +"Seçtiğiniz noktalar iki eğrinin uç noktaları değildir. Eğriler bir uç" +" noktasını " +"paylaşmazlar.\n" +"\n" +"Birbirlerine teğet olmalarını istediğiniz her iki eğrinin uç noktalarını" +" seçin." + +#: constraint.cpp:244 +msgid "" +"The curves must share an endpoint. Constrain them with Constrain -> On Point " +"before constraining tangent.\n" +"\n" +"Alternatively select the end points of both curves at which you want the " +"curves to be tangent." +msgstr "" +"Eğriler bir uç noktayı paylaşmalıdır. Teğet kısıtlamasından önce onları " +"Kısıtla -> Noktada ile kısıtlayın.\n" +"\n" +"Alternatif olarak, teğet olmasını istediğiniz eğrilerin iki uç noktalarını " +"seçin." + +#: constraint.cpp:303 +msgid "" +"Bad selection for distance / diameter constraint. This constraint can apply " +"to:\n" +"\n" +" * two points (distance between points)\n" +" * a line segment (length)\n" +" * two points and a line segment or normal (projected distance)\n" +" * a workplane and a point (minimum distance)\n" +" * a line segment and a point (minimum distance)\n" +" * a plane face and a point (minimum distance)\n" +" * a circle or an arc (diameter)\n" +msgstr "" +"Mesafe / çap kısıtlaması oluşturmak için hatalı seçim. Bu kısıtlama şunlara " +"uygulanabilir:\n" +"\n" +" * iki nokta (noktalar arası mesafe)\n" +" * bir çizgi parçası (uzunluk)\n" +" * iki nokta ve bir çizgi parçası veya normal (izdüşüm mesafesi)\n" +" * bir çalışma düzlemi ve bir nokta (minimum mesafe)\n" +" * bir çizgi parçası ve bir nokta (minimum mesafe)\n" +" * bir düzlem yüzeyi ve bir nokta (minimum mesafe)\n" +" * bir daire veya yay (çap)\n" + +#: constraint.cpp:366 +msgid "" +"Bad selection for on point / curve / plane constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points (points coincident)\n" +" * a point and a workplane (point in plane)\n" +" * a point and a line segment (point on line)\n" +" * a point and a circle or arc (point on curve)\n" +" * a point and one to three plane faces (point on face(s))\n" +msgstr "" +"Noktada / Eğride / Düzlemde kısıtlaması oluşturmak için hatalı seçim. Bu " +"kısıtlama şunlara uygulanabilir:\n" +"\n" +" * iki nokta (çakışan noktalar)\n" +" * bir nokta ve bir çalışma düzlemi (nokta ile düzlem çakışır)\n" +" * bir nokta ve bir çizgi parçası (nokta ile çizgi çakışır)\n" +" * bir nokta ve bir daire veya yay (nokta ile eğri çakışır)\n" +" * bir nokta ve bir düzlem yüzeyi (nokta ile yüzey çakışır)\n" + +#: constraint.cpp:427 +msgid "" +"Bad selection for equal length / radius constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more line segments (equal length)\n" +" * two line segments and two points (equal point-line distances)\n" +" * a line segment and two points (equal point-line distances)\n" +" * a line segment, and a point and line segment (point-line distance " +"equals length)\n" +" * two or more circles or arcs (equal radius)\n" +" * a line segment and an arc (line segment length equals arc length)\n" +msgstr "" +"Eşit uzunluk / yarıçap kısıtlaması oluşturmak için hatalı seçim. Bu " +"kısıtlama şunlara uygulanabilir:\n" +"\n" +" * iki veya daha fazla çizgi parçası (eşit uzunlukta)\n" +" * iki çizgi parçası ve iki nokta (eşit nokta-çizgi mesafeleri)\n" +" * bir çizgi parçası ve iki nokta (eşit nokta-çizgi mesafeleri)\n" +" * bir çizgi parçası ile bir nokta ve çizgi parçası (nokta-çizgi mesafesi " +"uzunluğu eşit)\n" +" * iki veya daha fazla daire veya yay (eşit yarıçap)\n" +" * bir çizgi parçası ve bir yay (çizgi parçası uzunluğu yay uzunluğuna " +"eşittir)\n" + +#: constraint.cpp:480 +msgid "" +"Bad selection for length ratio constraint. This constraint can apply to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"Uzunluk oranı kısıtlaması oluşturmak için hatalı seçim. Bu kısıtlama " +"aşağıdakiler için geçerli olabilir:\n" +"\n" +" * iki çizgi parçası\n" +" * iki yay\n" +" * bir yay ve bir çizgi parçası\n" + +#: constraint.cpp:515 +msgid "" +"Bad selection for length difference constraint. This constraint can apply " +"to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"Uzunluk farkı kısıtlaması oluşturmak için hatalı seçim. Bu kısıtlama " +"aşağıdakiler için geçerli olabilir:\n" +"\n" +" * iki çizgi parçası\n" +" * iki yay\n" +" * bir yay ve bir çizgi parçası\n" + +#: constraint.cpp:550 +msgid "" +"Bad selection for at midpoint constraint. This constraint can apply to:\n" +"\n" +" * a line segment and a point (point at midpoint)\n" +" * a line segment and a workplane (line's midpoint on plane)\n" +msgstr "" +"Orta nokta kısıtlaması oluşturmak için hatalı seçim. Bu kısıtlama şunlara " +"uygulanabilir:\n" +"\n" +" * bir çizgi parçası ve bir nokta (nokta çizgi ortasında)\n" +" * bir çizgi parçası ve bir çalışma düzlemi (çizgi düzlemin ortasına " +"taşınır )\n" + +#: constraint.cpp:608 +msgid "" +"Bad selection for symmetric constraint. This constraint can apply to:\n" +"\n" +" * two points or a line segment (symmetric about workplane's coordinate " +"axis)\n" +" * line segment, and two points or a line segment (symmetric about line " +"segment)\n" +" * workplane, and two points or a line segment (symmetric about " +"workplane)\n" +msgstr "" +"Simetri kısıtlaması oluşturmak için hatalı seçim. Bu kısıtlama şunlara " +"uygulanabilir:\n" +"\n" +" * iki nokta veya bir çizgi parçası (çalışma düzleminin koordinat " +"eksenine göre simetrik)\n" +" * çizgi parçası ve iki nokta veya bir çizgi parçası (çizgi parçası " +"etrafında simetrik)\n" +" * çalışma düzlemi ve iki nokta veya bir çizgi parçası (çalışma düzlemine " +"göre simetrik)\n" + +#: constraint.cpp:623 +msgid "" +"A workplane must be active when constraining symmetric without an explicit " +"symmetry plane." +msgstr "" +"Simetri kısıtlamasında simetri düzlemini olarak kullanılacak bir çalışma " +"düzlemi etkin olmalıdır." + +#: constraint.cpp:663 +msgid "" +"Activate a workplane (with Sketch -> In Workplane) before applying a " +"horizontal or vertical constraint." +msgstr "" +"Yatay veya dikey bir kısıtlama uygulamadan önce bir çalışma düzlemini (Çizim " +"-> Çalışma Düzleminde menüsü ile) etkinleştirin." + +#: constraint.cpp:679 +msgid "" +"Bad selection for horizontal / vertical constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points\n" +" * one or more line segments\n" +msgstr "" +"Yatay / Dikey kısıtlama oluşturmak için hatalı seçim. Bu kısıtlama şunlara " +"uygulanabilir:\n" +"\n" +" * iki nokta\n" +" * bir çizgi parçası\n" + +#: constraint.cpp:697 +msgid "" +"Bad selection for same orientation constraint. This constraint can apply " +"to:\n" +"\n" +" * two normals\n" +msgstr "" +"Aynı yön kısıtlaması oluşturmak için hatalı seçim. Bu kısıtlama şunlara " +"uygulanabilir:\n" +"\n" +" * iki normal\n" + +#: constraint.cpp:748 +msgid "Must select an angle constraint." +msgstr "Bir açı kısıtlaması seçilmelidir." + +#: constraint.cpp:761 +msgid "Must select a constraint with associated label." +msgstr "İlgili etikete sahip bir kısıtlama seçilmelidir." + +#: constraint.cpp:784 +msgid "" +"Bad selection for angle constraint. This constraint can apply to:\n" +"\n" +"Angle between:\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +"\n" +"Equal angles:\n" +" * four line segments or normals (equal angle between A,B and C,D)\n" +" * three line segments or normals (equal angle between A,B and B,C)\n" +msgstr "" +"Açı kısıtlaması oluşturmak için hatalı seçim. Bu kısıtlama şunlara " +"uygulanabilir:\n" +"\n" +"Şunların arasındaki açı:\n" +" * iki çizgi parçası\n" +" * bir çizgi parçası ve bir normal\n" +" * iki normal\n" +"\n" +"Eşit açılar:\n" +" * dört çizgi parçası veya normal (A,B ve C,D arasında eşit açı)\n" +" * üç çizgi parçası veya normal (A,B ve B,C arasında eşit açı)\n" + +#: constraint.cpp:872 +msgid "Curve-curve tangency must apply in workplane." +msgstr "Eğri-eğri teğetliği çalışma düzlemine uygulanmalıdır." + +#: constraint.cpp:887 +msgid "" +"Bad selection for parallel / tangent constraint. This constraint can apply " +"to:\n" +"\n" +" * two faces\n" +" * two or more line segments (parallel)\n" +" * one or more line segments and one or more normals (parallel)\n" +" * two or more normals (parallel)\n" +" * two line segments, arcs, or beziers, that share an endpoint (tangent)\n" +" * two line segments, arcs, or beziers, that do not share an endpoint and " +"the end point(s) of the curve(s) (tangent)\n" +msgstr "" +"Paralel / Teğet kısıtlaması oluşturmak için hatalı seçim. Bu kısıtlama " +"şunlara uygulanabilir:\n" +"\n" +" * iki yüzey\n" +" * iki veya daha fazla doğru parçası (paralel)\n" +" * bir veya daha fazla doğru parçası ve bir veya daha fazla normal " +"(paralel)\n" +" * iki veya daha fazla normal (paralel)\n" +" * bir uç noktayı (teğet) paylaşan iki doğru parçası, yay veya bezier " +"eğriler\n" +" *bir uç noktayı paylaşmayan iki doğru parçası, yay veya bezier eğriler ve " +"eğri(ler)in son noktası(ları) (teğet)\n" + +#: constraint.cpp:914 +msgid "" +"Bad selection for perpendicular constraint. This constraint can apply to:\n" +"\n" +" * two faces\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +msgstr "" +"Dikey kısıtlama oluşturmak için hatalı seçim. Bu kısıtlama şunlara " +"uygulanabilir:\n" +"\n" +" * iki yüzey\n" +" * iki çizgi parçası\n" +" * bir çizgi parçası ve bir normal\n" +" * iki normal\n" + +#: constraint.cpp:931 +msgid "" +"Bad selection for lock point where dragged constraint. This constraint can " +"apply to:\n" +"\n" +" * a point\n" +msgstr "" +"Sürüklendiği yerde noktayı kısıtlamak için hatalı seçim. Bu kısıtlama " +"şunlara uygulanabilir:\n" +"\n" +" * bir nokta\n" + +#: constraint.cpp:946 mouse.cpp:1160 +msgid "NEW COMMENT -- DOUBLE-CLICK TO EDIT" +msgstr "YENI YORUM -- DÜZENLEMEK ICIN CIFT-TIKLAYIN" + +#: constraint.cpp:952 +msgid "click center of comment text" +msgstr "yorum metninin ortasını tıklatın" + +#: export.cpp:19 +msgid "" +"No solid model present; draw one with extrudes and revolves, or use Export " +"2d View to export bare lines and curves." +msgstr "" +"Katı model yok; katılama ve döndürme komutları ile bir model çizin veya açık " +"çizgileri ve eğrileri dışa vermek için Görüntüyü 2d olarak Dışa Aktar'ı " +"kullanın." + +#: export.cpp:61 +msgid "" +"Bad selection for export section. Please select:\n" +"\n" +" * nothing, with an active workplane (workplane is section plane)\n" +" * a face (section plane through face)\n" +" * a point and two line segments (plane through point and parallel to " +"lines)\n" +msgstr "" +"Kesiti Dışa Aktarmak için hatalı seçim. Lütfen şunlardan birini seçin:\n" +"\n" +" * aktif bir çalışma düzleminde iken hiçbir şey seçmeyin (çalışma " +"düzlemi, kesit düzlemidir)\n" +" * bir yüzey (yüzeyden kesit düzlemi)\n" +" * bir nokta ve iki çizgi parçası (nokta ve paralel çizgiler boyunca " +"düzlem)\n" + +#: export.cpp:818 +msgid "Active group mesh is empty; nothing to export." +msgstr "Etkin grup ağı boş; dışa aktarılacak bir şey yok." + +#: exportvector.cpp:336 +msgid "freehand lines were replaced with continuous lines" +msgstr "serbest çizgiler, sürekli çizgilerle değiştirildi" + +#: exportvector.cpp:338 +msgid "zigzag lines were replaced with continuous lines" +msgstr "zikzak çizgiler sürekli çizgilerle değiştirildi" + +#: exportvector.cpp:592 +msgid "" +"Some aspects of the drawing have no DXF equivalent and were not exported:\n" +msgstr "Çizimin bazı yönlerinin DXF eşdeğeri yoktur ve dışa aktarılmamıştır:\n" + +#: exportvector.cpp:838 +msgid "" +"PDF page size exceeds 200 by 200 inches; many viewers may reject this file." +msgstr "" +"PDF sayfa boyutu 200 x 200 inç'i aşıyor; birçok görüntüleyici bu dosyayı " +"reddedebilir." + +#: file.cpp:44 group.cpp:91 +msgctxt "group-name" +msgid "sketch-in-plane" +msgstr "düzlemde çizim" + +#: file.cpp:62 +msgctxt "group-name" +msgid "#references" +msgstr "#referanslar" + +#: file.cpp:555 +msgid "The file is empty. It may be corrupt." +msgstr "Dosya boş. Bozuk olabilir." + +#: file.cpp:560 +msgid "" +"Unrecognized data in file. This file may be corrupt, or from a newer version " +"of the program." +msgstr "" +"Dosyada veriler tanınmadı. Bu dosya bozuk veya programın daha yeni bir " +"sürümü ile oluşturulmuş olabilir." + +#: file.cpp:876 +msgctxt "title" +msgid "Missing File" +msgstr "Kayıp Dosya" + +#: file.cpp:877 +#, c-format +msgctxt "dialog" +msgid "The linked file “%s” is not present." +msgstr "\"%s\" bağlantılı dosya yok." + +#: file.cpp:879 +msgctxt "dialog" +msgid "" +"Do you want to locate it manually?\n" +"\n" +"If you decline, any geometry that depends on the missing file will be " +"permanently removed." +msgstr "" +"Konumu El ile bulmak istiyor musunuz?\n" +"\n" +"Reddederseniz, eksik dosyaya bağlı olan geometri kalıcı olarak " +"kaldırılacaktır." + +#: file.cpp:882 +msgctxt "button" +msgid "&Yes" +msgstr "&Evet" + +#: file.cpp:884 +msgctxt "button" +msgid "&No" +msgstr "&Hayır" + +#: file.cpp:886 solvespace.cpp:652 +msgctxt "button" +msgid "&Cancel" +msgstr "&İptal" + +#: graphicswin.cpp:41 +msgid "&File" +msgstr "&Dosya" + +#: graphicswin.cpp:42 +msgid "&New" +msgstr "&Yeni" + +#: graphicswin.cpp:43 +msgid "&Open..." +msgstr "&Aç..." + +#: graphicswin.cpp:44 +msgid "Open &Recent" +msgstr "&Son Erişilenden Aç" + +#: graphicswin.cpp:45 +msgid "&Save" +msgstr "&Kaydet" + +#: graphicswin.cpp:46 +msgid "Save &As..." +msgstr "&Farklı kaydet..." + +#: graphicswin.cpp:48 +msgid "Export &Image..." +msgstr "&Resim olarak dışa aktar..." + +#: graphicswin.cpp:49 +msgid "Export 2d &View..." +msgstr "&Görüntüyü 2d olarak dışa aktar..." + +#: graphicswin.cpp:50 +msgid "Export 2d &Section..." +msgstr "&Kesiti 2d olarak dışa aktar..." + +#: graphicswin.cpp:51 +msgid "Export 3d &Wireframe..." +msgstr "3d &TelKafes olarak dışa aktar..." + +#: graphicswin.cpp:52 +msgid "Export Triangle &Mesh..." +msgstr "&Üçgensel Ağ olarak dışa aktar..." + +#: graphicswin.cpp:53 +msgid "Export &Surfaces..." +msgstr "Y&üzeyleri dışa aktar..." + +#: graphicswin.cpp:54 +msgid "Im&port..." +msgstr "&İçe Aktar..." + +#: graphicswin.cpp:57 +msgid "E&xit" +msgstr "&Çıkış" + +#: graphicswin.cpp:60 +msgid "&Edit" +msgstr "D&üzen" + +#: graphicswin.cpp:61 +msgid "&Undo" +msgstr "&Geri al" + +#: graphicswin.cpp:62 +msgid "&Redo" +msgstr "&Yinele" + +#: graphicswin.cpp:63 +msgid "Re&generate All" +msgstr "Tümünü Yeniden &Oluştur" + +#: graphicswin.cpp:65 +msgid "Snap Selection to &Grid" +msgstr "Seçimi &Izgaraya Tuttur" + +#: graphicswin.cpp:66 +msgid "Rotate Imported &90°" +msgstr "İçe Aktarılanları &90° Döndür" + +#: graphicswin.cpp:68 +msgid "Cu&t" +msgstr "&Kes" + +#: graphicswin.cpp:69 +msgid "&Copy" +msgstr "K&opyala" + +#: graphicswin.cpp:70 +msgid "&Paste" +msgstr "Y&apıştır" + +#: graphicswin.cpp:71 +msgid "Paste &Transformed..." +msgstr "&Dönüştürerek Yapıştır..." + +#: graphicswin.cpp:72 +msgid "&Delete" +msgstr "&Sil" + +#: graphicswin.cpp:74 +msgid "Select &Edge Chain" +msgstr "Kenar &Zincirini Seç" + +#: graphicswin.cpp:75 +msgid "Select &All" +msgstr "&Tümünü Seç" + +#: graphicswin.cpp:76 +msgid "&Unselect All" +msgstr "Tüm Seçimi &Kaldır" + +#: graphicswin.cpp:78 +msgid "&Line Styles..." +msgstr "&Çizgi Biçimi..." + +#: graphicswin.cpp:79 +msgid "&View Projection..." +msgstr "&Projeksiyonu Görüntüle..." + +#: graphicswin.cpp:81 +msgid "Con&figuration..." +msgstr "Y&apılandır..." + +#: graphicswin.cpp:84 +msgid "&View" +msgstr "&Görünüm" + +#: graphicswin.cpp:85 +msgid "Zoom &In" +msgstr "&Yakınlaş" + +#: graphicswin.cpp:86 +msgid "Zoom &Out" +msgstr "&Uzaklaş" + +#: graphicswin.cpp:87 +msgid "Zoom To &Fit" +msgstr "&Sığacak Şekilde Yakınlaş" + +#: graphicswin.cpp:89 +msgid "Align View to &Workplane" +msgstr "Görünümü &Çalışma Düzlemine Hizala" + +#: graphicswin.cpp:90 +msgid "Nearest &Ortho View" +msgstr "En Yakın &Dik Görünüm" + +#: graphicswin.cpp:91 +msgid "Nearest &Isometric View" +msgstr "En Yakın &İzometrik Görünüm" + +#: graphicswin.cpp:92 +msgid "&Center View At Point" +msgstr "&Noktayı Merkezde Görüntüle" + +#: graphicswin.cpp:94 +msgid "Show Snap &Grid" +msgstr "Yakalama &Izgarasını Göster" + +#: graphicswin.cpp:95 +msgid "Darken Inactive Solids" +msgstr "Aktif Olmayan Katıları &Koyulaştır" + +#: graphicswin.cpp:96 +msgid "Use &Perspective Projection" +msgstr "&Perspektif Projeksiyonu Kullan" + +#: graphicswin.cpp:97 +msgid "Show E&xploded View" +msgstr "Pat&latılmış Görünümü Göster" + +#: graphicswin.cpp:98 +msgid "Dimension &Units" +msgstr "Ölçü &Birimleri" + +#: graphicswin.cpp:99 +msgid "Dimensions in &Millimeters" +msgstr "&Milimetre cinsinden ölçü" + +#: graphicswin.cpp:100 +msgid "Dimensions in M&eters" +msgstr "M&etre cinsinden ölçü" + +#: graphicswin.cpp:101 +msgid "Dimensions in &Inches" +msgstr "&İnç cinsinden ölçü" + +#: graphicswin.cpp:102 +msgid "Dimensions in &Feet and Inches" +msgstr "&Fit ve İnç cinsinden ölçü" + +#: graphicswin.cpp:104 +msgid "Show &Toolbar" +msgstr "&Araç Çubuğunu Göster" + +#: graphicswin.cpp:105 +msgid "Show Property Bro&wser" +msgstr "&Özellik Penceresini Göster" + +#: graphicswin.cpp:107 +msgid "&Full Screen" +msgstr "&Tam Ekran" + +#: graphicswin.cpp:109 +msgid "&New Group" +msgstr "Yeni &Grup" + +#: graphicswin.cpp:110 +msgid "Sketch In &3d" +msgstr "&3d'de Çizim Yap" + +#: graphicswin.cpp:111 +msgid "Sketch In New &Workplane" +msgstr "&Yeni Çalışma Düzleminde Çizim Yap" + +#: graphicswin.cpp:113 +msgid "Step &Translating" +msgstr "&Doğrusal Kopya" + +#: graphicswin.cpp:114 +msgid "Step &Rotating" +msgstr "D&airesel Kopya" + +#: graphicswin.cpp:116 +msgid "E&xtrude" +msgstr "&Uzatarak Katıla" + +#: graphicswin.cpp:117 +msgid "&Helix" +msgstr "&Helis" + +#: graphicswin.cpp:118 +msgid "&Lathe" +msgstr "&Tam döndürerek katıla" + +#: graphicswin.cpp:119 +msgid "Re&volve" +msgstr "&Kısmen döndürerek katıla" + +#: graphicswin.cpp:121 +msgid "Link / Assemble..." +msgstr "Bağla / Montajla..." + +#: graphicswin.cpp:122 +msgid "Link Recent" +msgstr "Son Erişilenden Bağla" + +#: graphicswin.cpp:124 +msgid "&Sketch" +msgstr "&Çizim" + +#: graphicswin.cpp:125 +msgid "In &Workplane" +msgstr "Ç&alışma Düzleminde" + +#: graphicswin.cpp:126 +msgid "Anywhere In &3d" +msgstr "&3d'de Herhangi Bir Yerde" + +#: graphicswin.cpp:128 +msgid "Datum &Point" +msgstr "Referasn &Noktası" + +#: graphicswin.cpp:129 +msgid "Wor&kplane" +msgstr "Ça&lışma Düzlemi" + +#: graphicswin.cpp:131 +msgid "Line &Segment" +msgstr "Ç&izgi" + +#: graphicswin.cpp:132 +msgid "C&onstruction Line Segment" +msgstr "&Yardımcı Çizgi" + +#: graphicswin.cpp:133 +msgid "&Rectangle" +msgstr "&Dikdörtgen" + +#: graphicswin.cpp:134 +msgid "&Circle" +msgstr "D&aire" + +#: graphicswin.cpp:135 +msgid "&Arc of a Circle" +msgstr "Çember &Yayı" + +#: graphicswin.cpp:136 +msgid "&Bezier Cubic Spline" +msgstr "Bezier Kübik &Eğri" + +#: graphicswin.cpp:138 +msgid "&Text in TrueType Font" +msgstr "TrueType Yazı Tipinde &Metin" + +#: graphicswin.cpp:139 +msgid "I&mage" +msgstr "&Resim / Görüntü" + +#: graphicswin.cpp:141 +msgid "To&ggle Construction" +msgstr "Yap&ıyı Değiştir" + +#: graphicswin.cpp:142 +msgid "Ta&ngent Arc at Point" +msgstr "&Yuvarlat (Radyus)" + +#: graphicswin.cpp:143 +msgid "Split Curves at &Intersection" +msgstr "Eğrileri Kesişim Yerinden &Böl" + +#: graphicswin.cpp:145 +msgid "&Constrain" +msgstr "&Kısıtla" + +#: graphicswin.cpp:146 +msgid "&Distance / Diameter" +msgstr "&Mesafe / Çap" + +#: graphicswin.cpp:147 +msgid "Re&ference Dimension" +msgstr "&Referans Ölçü" + +#: graphicswin.cpp:148 +msgid "A&ngle / Equal Angle" +msgstr "&Açı / Eşit Açı" + +#: graphicswin.cpp:149 +msgid "Reference An&gle" +msgstr "Referans A&çı" + +#: graphicswin.cpp:150 +msgid "Other S&upplementary Angle" +msgstr "Diğer &Bütünler Açı" + +#: graphicswin.cpp:151 +msgid "Toggle R&eference Dim" +msgstr "Ölçüyü Re&ferans Yap / Yapma" + +#: graphicswin.cpp:153 +msgid "&Horizontal" +msgstr "&Yatay" + +#: graphicswin.cpp:154 +msgid "&Vertical" +msgstr "&Dikey" + +#: graphicswin.cpp:156 +msgid "&On Point / Curve / Plane" +msgstr "&Noktada / Eğride / Düzlemde" + +#: graphicswin.cpp:157 +msgid "E&qual Length / Radius" +msgstr "&Eşit Uzunluk / Yarıçap" + +#: graphicswin.cpp:158 +msgid "Length / Arc Ra&tio" +msgstr "&Uzunluk / Yay Oranı" + +#: graphicswin.cpp:159 +msgid "Length / Arc Diff&erence" +msgstr "Uzunluk / Yay &Farkı" + +#: graphicswin.cpp:160 +msgid "At &Midpoint" +msgstr "&Orta Noktada" + +#: graphicswin.cpp:161 +msgid "S&ymmetric" +msgstr "&Simetrik" + +#: graphicswin.cpp:162 +msgid "Para&llel / Tangent" +msgstr "&Paralel / Teğet" + +#: graphicswin.cpp:163 +msgid "&Perpendicular" +msgstr "D&ik" + +#: graphicswin.cpp:164 +msgid "Same Orient&ation" +msgstr "Aynı &Yön" + +#: graphicswin.cpp:165 +msgid "Lock Point Where &Dragged" +msgstr "Sürüklendiği Yerde Noktayı &Kilitle" + +#: graphicswin.cpp:167 +msgid "Comment" +msgstr "Y&orum" + +#: graphicswin.cpp:169 +msgid "&Analyze" +msgstr "&Analiz" + +#: graphicswin.cpp:170 +msgid "Measure &Volume" +msgstr "&Hacmi Hesapla" + +#: graphicswin.cpp:171 +msgid "Measure A&rea" +msgstr "&Alanı Hesapla" + +#: graphicswin.cpp:172 +msgid "Measure &Perimeter" +msgstr "&Çevre Uzunluğunu Hesapla" + +#: graphicswin.cpp:173 +msgid "Show &Interfering Parts" +msgstr "&Engelleyici Parçaları Göster" + +#: graphicswin.cpp:174 +msgid "Show &Naked Edges" +msgstr "A&çık Kenarları Göster" + +#: graphicswin.cpp:175 +msgid "Show &Center of Mass" +msgstr "&Kütle Merkezini Göster" + +#: graphicswin.cpp:177 +msgid "Show &Underconstrained Points" +msgstr "Kı&sıtlanmamış Noktaları Göster" + +#: graphicswin.cpp:179 +msgid "&Trace Point" +msgstr "&İzlenecek Nokta" + +#: graphicswin.cpp:180 +msgid "&Stop Tracing..." +msgstr "&İzlemeyi Durdur..." + +#: graphicswin.cpp:181 +msgid "Step &Dimension..." +msgstr "Adım &Ölçüsünü Ayarla..." + +#: graphicswin.cpp:183 +msgid "&Help" +msgstr "&Yardım" + +#: graphicswin.cpp:184 +msgid "&Language" +msgstr "&Dil" + +#: graphicswin.cpp:185 +msgid "&Website / Manual" +msgstr "&Web sitesi / Kılavuz" + +#: graphicswin.cpp:186 +msgid "&Go to GitHub commit" +msgstr "GitHub Commitlere git" + +#: graphicswin.cpp:188 +msgid "&About" +msgstr "&Hakkında" + +#: graphicswin.cpp:362 +msgid "(no recent files)" +msgstr "(son dosyalar yok)" + +#: graphicswin.cpp:370 +#, c-format +msgid "File '%s' does not exist." +msgstr "'%s' dosyası mevcut değil." + +#: graphicswin.cpp:779 +msgid "No workplane is active, so the grid will not appear." +msgstr "Etkin çalışma düzlemi yok, bu nedenle ızgara görünmeyecektir." + +#: graphicswin.cpp:794 +msgid "" +"The perspective factor is set to zero, so the view will always be a parallel " +"projection.\n" +"\n" +"For a perspective projection, modify the perspective factor in the " +"configuration screen. A value around 0.3 is typical." +msgstr "" +"Perspektif katsayısı sıfıra ayarlanmıştır, bu nedenle görünüm her zaman " +"paralel bir projeksiyon olacaktır.\n" +"\n" +"Perspektif bir projeksiyon için, yapılandırma ekranındaki perspektif " +"katsayısını değiştirin. 0.3 civarında bir değer normaldir." + +#: graphicswin.cpp:884 +msgid "" +"Select a point; this point will become the center of the view on screen." +msgstr "Bir nokta seçin; bu nokta ekrandaki görüntünün merkezi olacaktır." + +#: graphicswin.cpp:1193 +msgid "No additional entities share endpoints with the selected entities." +msgstr "Hiçbir ek öğe, seçili öğeler ile uç noktaları paylaşmıyor." + +#: graphicswin.cpp:1211 +msgid "" +"To use this command, select a point or other entity from an linked part, or " +"make a link group the active group." +msgstr "" +"Bu komutu kullanmak için, bağlantılı bir parçadan bir nokta veya başka bir " +"öğe seçin veya bir bağlantı grubunu etkin grup haline getirin." + +#: graphicswin.cpp:1234 +msgid "" +"No workplane is active. Activate a workplane (with Sketch -> In Workplane) " +"to define the plane for the snap grid." +msgstr "" +"Hiçbir çalışma düzlemi etkin değil. Izgaranın yakalanacağı düzlemini " +"tanımlamak için bir çalışma düzlemini (Çizim -> Çalışma Düzleminde ile) " +"etkinleştirin." + +#: graphicswin.cpp:1241 +msgid "" +"Can't snap these items to grid; select points, text comments, or constraints " +"with a label. To snap a line, select its endpoints." +msgstr "" +"Bu öğeler ızgaraya tutturulamıyor; etiketli noktaları, metin yorumlarını " +"veya kısıtlamaları seçin. Bir çizgiyi tutturmak için uç noktalarını seçin." + +#: graphicswin.cpp:1326 +msgid "No workplane selected. Activating default workplane for this group." +msgstr "" +"Çalışma düzlemi seçilmedi. Bu grup için varsayılan çalışma düzlemi " +"etkinleştiriliyor." + +#: graphicswin.cpp:1329 +msgid "" +"No workplane is selected, and the active group does not have a default " +"workplane. Try selecting a workplane, or activating a sketch-in-new-" +"workplane group." +msgstr "" +"Hiçbir çalışma düzlemi seçilmedi ve aktif grubun varsayılan bir çalışma " +"düzlemi yok. Bir çalışma düzlemi seçmeyi veya bir yeni çalışma düzleminde " +"çizim grubunu etkinleştirmeyi deneyin." + +#: graphicswin.cpp:1350 +msgid "" +"Bad selection for tangent arc at point. Select a single point, or select " +"nothing to set up arc parameters." +msgstr "" +"Noktada teğet yay (radyus) oluşturmak için hatalı seçim. Tek bir nokta seçin " +"veya yay parametrelerini ayarlamak için hiçbir şey seçmeyin." + +#: graphicswin.cpp:1361 +msgid "click point on arc (draws anti-clockwise)" +msgstr "yayın ilk noktası için tıklayın (saat yönünün tersine çizilir)" + +#: graphicswin.cpp:1362 +msgid "click to place datum point" +msgstr "referans noktayı yerleştirmek için tıklayın" + +#: graphicswin.cpp:1363 +msgid "click first point of line segment" +msgstr "çizgi parçasının ilk noktası için tıklayın" + +#: graphicswin.cpp:1365 +msgid "click first point of construction line segment" +msgstr "yardımcı çizginin ilk noktası için tıklayın" + +#: graphicswin.cpp:1366 +msgid "click first point of cubic segment" +msgstr "kübik eğri parçanın ilk noktası için tıklayın" + +#: graphicswin.cpp:1367 +msgid "click center of circle" +msgstr "dairenin merkez konumu için tıklayın" + +#: graphicswin.cpp:1368 +msgid "click origin of workplane" +msgstr "çalışma düzleminin merkez konumu için tıklayın" + +#: graphicswin.cpp:1369 +msgid "click one corner of rectangle" +msgstr "dikdörtgenin bir köşe noktası için tıklayın" + +#: graphicswin.cpp:1370 +msgid "click top left of text" +msgstr "metnin sol üst köşe konumu için tıklayın" + +#: graphicswin.cpp:1376 +msgid "click top left of image" +msgstr "görüntünün sol üst köşe konumu için tıklayın" + +#: graphicswin.cpp:1402 +msgid "" +"No entities are selected. Select entities before trying to toggle their " +"construction state." +msgstr "" +"Hiçbir öğe seçilmedi. Yapı durumlarını değiştirmeye çalışmadan önce öğeleri " +"seçin." + +#: group.cpp:86 +msgctxt "group-name" +msgid "sketch-in-3d" +msgstr "3d-de-çizim" + +#: group.cpp:154 +msgid "" +"Bad selection for new sketch in workplane. This group can be created with:\n" +"\n" +" * a point (through the point, orthogonal to coordinate axes)\n" +" * a point and two line segments (through the point, parallel to the " +"lines)\n" +" * a point and a normal (through the point, orthogonal to the normal)\n" +" * a workplane (copy of the workplane)\n" +msgstr "" +"Çalışma düzleminde yeni çizim oluşturmak için hatalı seçim. Bu grup " +"aşağıdakilerle oluşturulabilir:\n" +"\n" +" * bir nokta (noktadan geçen, koordinat eksenine dik)\n" +" * bir nokta ve iki doğru parçası (noktadan geçen, doğrulara paralel)\n" +" * bir nokta ve bir normal (noktadan normale dik)\n" +" * bir çalışma düzlemi (çalışma düzleminin kopyası)\n" + +#: group.cpp:170 +msgid "" +"Activate a workplane (Sketch -> In Workplane) before extruding. The sketch " +"will be extruded normal to the workplane." +msgstr "" +"Katılama işleminden önce bir çalışma düzlemini etkinleştirin. Çizim, " +"çalışma düzlemine dik olarak katılanacaktır." + +#: group.cpp:179 +msgctxt "group-name" +msgid "extrude" +msgstr "doğrusal katıla" + +#: group.cpp:184 +msgid "Lathe operation can only be applied to planar sketches." +msgstr "" +"Tam tur döndürerek katılama işlemi, yalnızca düzlemsel çizimlere " +"uygulanabilir." + +#: group.cpp:195 +msgid "" +"Bad selection for new lathe group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Tamamen döndürerek yeni katılama grubu oluşturmak için hatalı seçim. Bu grup " +"şu şekilde oluşturulabilir:\n" +"\n" +" * bir nokta ve bir çizgi parçası veya normal (çizgiye / normale paralel " +"bir eksen etrafında, nokta boyunca döndürülür )\n" +" * bir çizgi parçası (çizgi parçası etrafında döndürülür)\n" + +#: group.cpp:205 +msgctxt "group-name" +msgid "lathe" +msgstr "tamamen döndürerek katıla" + +#: group.cpp:210 +msgid "Revolve operation can only be applied to planar sketches." +msgstr "" +"Kısmen döndürerek katılama işlemi, yalnızca düzlemsel çizimlere " +"uygulanabilir." + +#: group.cpp:221 +msgid "" +"Bad selection for new revolve group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Kısmen döndürerek yeni katılama grubu oluşturmak için hatalı seçim. Bu grup " +"şu şekilde oluşturulabilir:\n" +"\n" +" * bir nokta ve bir çizgi parçası veya normal (çizgiye / normale paralel, " +"noktadan geçen bir eksen etrafında döndürülür)\n" +" * bir çizgi parçası (çizgi parçası etrafında döndürülür)\n" + +#: group.cpp:233 +msgctxt "group-name" +msgid "revolve" +msgstr "kısmen döndürerek katıla" + +#: group.cpp:238 +msgid "Helix operation can only be applied to planar sketches." +msgstr "Helis işlemi yalnızca düzlemsel çizimlere uygulanabilir." + +#: group.cpp:249 +msgid "" +"Bad selection for new helix group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Yeni helis grubu oluşturmak için hatalı seçim. Bu grup şunlarla " +"oluşturulabilir:\n" +"\n" +" * bir nokta ve bir çizgi parçası veya normal (çizgiye / normale paralel, " +"noktadan geçen bir eksen etrafında döndürülür)\n" +" * bir çizgi parçası (çizgi parçası etrafında döndürülür)\n" + +#: group.cpp:261 +msgctxt "group-name" +msgid "helix" +msgstr "helis" + +#: group.cpp:274 +msgid "" +"Bad selection for new rotation. This group can be created with:\n" +"\n" +" * a point, while locked in workplane (rotate in plane, about that " +"point)\n" +" * a point and a line or a normal (rotate about an axis through the " +"point, and parallel to line / normal)\n" +msgstr "" +"Yeni dairesel kopyalama/çoğaltma oluşturmak için hatalı seçim. Bu grup " +"şunlarla oluşturulabilir:\n" +"\n" +" * bir nokta, çalışma düzleminde kilitli olduğu sürece (düzlemde o nokta " +"etrafında çevirin)\n" +" * bir nokta ve bir çizgi veya bir normal ( çizgiye / normale paralel ve " +"noktadan geçen bir eksen etrafında çevirin)\n" + +#: group.cpp:287 +msgctxt "group-name" +msgid "rotate" +msgstr "dairesel kopya" + +#: group.cpp:298 +msgctxt "group-name" +msgid "translate" +msgstr "doğrusal kopya" + +#: group.cpp:422 +msgid "(unnamed)" +msgstr "(isimsiz)" + +#: groupmesh.cpp:710 +msgid "not closed contour, or not all same style!" +msgstr "kapali olmayan kontur veya tümü ayni bicimde degil!" + +#: groupmesh.cpp:723 +msgid "points not all coplanar!" +msgstr "noktaların hepsi aynı düzlemde değil!" + +#: groupmesh.cpp:725 +msgid "contour is self-intersecting!" +msgstr "kontur kendisiyle kesisiyor!" + +#: groupmesh.cpp:727 +msgid "zero-length edge!" +msgstr "sıfır-uzunluklu kenar!" + +#: importmesh.cpp:136 +msgid "Text-formated STL files are not currently supported" +msgstr "Metin-biçimli STL dosyaları şu anda desteklenmiyor" + +#: modify.cpp:252 +msgid "Must be sketching in workplane to create tangent arc." +msgstr "Teğet yay oluşturmak için çalışma düzleminde çizim yapılmalıdır." + +#: modify.cpp:299 +msgid "" +"To create a tangent arc, select a point where two non-construction lines or " +"circles in this group and workplane join." +msgstr "" +"Bir teğet yay oluşturmak için, bu gruptaki iki (yardımcı) referans-olmayan " +"çizginin veya dairenin ve çalışma düzleminin birleştiği bir nokta seçin." + +#: modify.cpp:386 +msgid "" +"Couldn't round this corner. Try a smaller radius, or try creating the " +"desired geometry by hand with tangency constraints." +msgstr "" +"Bu köşe yuvarlatılamadı. Daha küçük bir yarıçap deneyin veya teğet " +"kısıtlamaları ile istenen geometriyi elle oluşturmayı deneyin." + +#: modify.cpp:595 +msgid "Couldn't split this entity; lines, circles, or cubics only." +msgstr "" +"Bu öğe bölünemedi; yalnızca çizgiler, daireler veya kübik eğriler " +"bölünebilir." + +#: modify.cpp:622 +msgid "Must be sketching in workplane to split." +msgstr "Bölmek için çalışma düzleminde çizim yapılıyor olmalı." + +#: modify.cpp:629 +msgid "" +"Select two entities that intersect each other (e.g. two lines/circles/arcs " +"or a line/circle/arc and a point)." +msgstr "" +"Birbiriyle kesişen iki öğe seçin (örneğin iki çizgi / daire / yay veya bir " +"çizgi / daire / yay ve bir nokta)." + +#: modify.cpp:734 +msgid "Can't split; no intersection found." +msgstr "Bölünemez; kesişim bulunamadı." + +#: mouse.cpp:558 +msgid "Assign to Style" +msgstr "Biçim Ata" + +#: mouse.cpp:574 +msgid "No Style" +msgstr "Biçim Yok" + +#: mouse.cpp:577 +msgid "Newly Created Custom Style..." +msgstr "Yeni Oluşturulan Özel Biçim ..." + +#: mouse.cpp:584 +msgid "Group Info" +msgstr "Grup Bilgisi" + +#: mouse.cpp:604 +msgid "Style Info" +msgstr "Biçim Bİlgisi" + +#: mouse.cpp:624 +msgid "Select Edge Chain" +msgstr "Kenar Zinciri Seç" + +#: mouse.cpp:630 +msgid "Toggle Reference Dimension" +msgstr "Referans Ölçüyü Değiştir" + +#: mouse.cpp:636 +msgid "Other Supplementary Angle" +msgstr "Diğer Bütünler Açı" + +#: mouse.cpp:641 +msgid "Snap to Grid" +msgstr "Izgaraya Tuttur" + +#: mouse.cpp:650 +msgid "Remove Spline Point" +msgstr "Eğri Noktasını Sil" + +#: mouse.cpp:685 +msgid "Add Spline Point" +msgstr "Eğri Noktası Ekle" + +#: mouse.cpp:689 +msgid "Cannot add spline point: maximum number of points reached." +msgstr "Eğri noktası eklenemiyor: en fazla nokta sayısına ulaşıldı." + +#: mouse.cpp:714 +msgid "Toggle Construction" +msgstr "Yapıyı değiştir" + +#: mouse.cpp:730 +msgid "Delete Point-Coincident Constraint" +msgstr "Çakışık-Nokta Kısıtlamasını Sil" + +#: mouse.cpp:748 +msgid "Cut" +msgstr "Kes" + +#: mouse.cpp:750 +msgid "Copy" +msgstr "Kopyala" + +#: mouse.cpp:754 +msgid "Select All" +msgstr "Tümünü Seç" + +#: mouse.cpp:759 +msgid "Paste" +msgstr "Yapıştır" + +#: mouse.cpp:761 +msgid "Paste Transformed..." +msgstr "Dönüştürerek Yapıştır..." + +#: mouse.cpp:766 +msgid "Delete" +msgstr "Sil" + +#: mouse.cpp:769 +msgid "Unselect All" +msgstr "Tüm Seçimi Kaldır" + +#: mouse.cpp:776 +msgid "Unselect Hovered" +msgstr "Üzerine Gelinen Seçimi Kaldır" + +#: mouse.cpp:785 +msgid "Zoom to Fit" +msgstr "Sığdırmak için Yakınlaştır" + +#: mouse.cpp:987 mouse.cpp:1276 +msgid "click next point of line, or press Esc" +msgstr "çizginin sonraki noktası için tıklayın veya Esc tuşuna basın" + +#: mouse.cpp:993 +msgid "" +"Can't draw rectangle in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"3d'de dikdörtgen çizilemez; önce Çizim -> Çalışma Düzleminde menüsü ile bir " +"çalışma düzlemini etkinleştirin." + +#: mouse.cpp:1027 +msgid "click to place other corner of rectangle" +msgstr "dikdörtgenin diğer köşesini belirtmek için tıklayın" + +#: mouse.cpp:1048 +msgid "click to set radius" +msgstr "yarıçapı ayarlamak için tıklayın" + +#: mouse.cpp:1053 +msgid "" +"Can't draw arc in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"3d'de yay çizemez; önce Çizim -> Çalışma Düzleminde menüsü ile bir çalışma " +"düzlemini etkinleştirin." + +#: mouse.cpp:1072 +msgid "click to place point" +msgstr "noktayı yerleştirmek için tıklayın" + +#: mouse.cpp:1088 +msgid "click next point of cubic, or press Esc" +msgstr "sonraki kübik eğri noktasını tıklayın veya Esc tuşuna basın" + +#: mouse.cpp:1093 +msgid "" +"Sketching in a workplane already; sketch in 3d before creating new workplane." +msgstr "" +"Zaten bir çalışma düzleminde çizim yapılıyor; 3d'de çizim yapmadan önce yeni " +"çalışma düzlemi oluşturun." + +#: mouse.cpp:1109 +msgid "" +"Can't draw text in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"3d'de metin yazılamaz; önce Çizim -> Çalışma Düzleminde menüsü ile bir " +"çalışma düzlemini etkinleştirin." + +#: mouse.cpp:1126 +msgid "click to place bottom right of text" +msgstr "metnin sağ alt konumunu belirtmek için tıklayın" + +#: mouse.cpp:1132 +msgid "" +"Can't draw image in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"3d'de görüntü/resim eklenemez; önce Çizim -> Çalışma Düzleminde menüsü ile " +"bir çalışma düzlemini etkinleştirin." + +#: platform/gui.cpp:85 platform/gui.cpp:90 solvespace.cpp:583 +msgctxt "file-type" +msgid "SolveSpace models" +msgstr "SolveSpace Modelleri" + +#: platform/gui.cpp:89 +msgctxt "file-type" +msgid "ALL" +msgstr "TÜMÜ" + +#: platform/gui.cpp:91 +msgctxt "file-type" +msgid "IDF circuit board" +msgstr "IDF devre kartı" + +#: platform/gui.cpp:92 +msgctxt "file-type" +msgid "STL triangle mesh" +msgstr "STL üçgensel mesh (ağ/kafes)" + +#: platform/gui.cpp:96 +msgctxt "file-type" +msgid "PNG image" +msgstr "PNG Görüntüsü" + +#: platform/gui.cpp:100 +msgctxt "file-type" +msgid "STL mesh" +msgstr "STL mesh (ağ/kafes)" + +#: platform/gui.cpp:101 +msgctxt "file-type" +msgid "Wavefront OBJ mesh" +msgstr "Wavefront OBJ mesh (ağ/kafes)" + +#: platform/gui.cpp:102 +msgctxt "file-type" +msgid "Three.js-compatible mesh, with viewer" +msgstr "Görüntüleyicili, Three.js-uyumlu mesh (ağ/kafes)" + +#: platform/gui.cpp:103 +msgctxt "file-type" +msgid "Three.js-compatible mesh, mesh only" +msgstr "Three.js-uyumlu mesh, sadece mesh (ağ/kafes)" + +#: platform/gui.cpp:104 +msgctxt "file-type" +msgid "VRML text file" +msgstr "VRML metin dosyası" + +#: platform/gui.cpp:108 platform/gui.cpp:115 platform/gui.cpp:122 +msgctxt "file-type" +msgid "STEP file" +msgstr "STEP dosyası" + +#: platform/gui.cpp:112 +msgctxt "file-type" +msgid "PDF file" +msgstr "PDF dosyası" + +#: platform/gui.cpp:113 +msgctxt "file-type" +msgid "Encapsulated PostScript" +msgstr "Encapsulated PostScript (EPS)" + +#: platform/gui.cpp:114 +msgctxt "file-type" +msgid "Scalable Vector Graphics" +msgstr "Ölçeklenebilir Vektör Grafikleri (SVG)" + +#: platform/gui.cpp:116 platform/gui.cpp:123 +msgctxt "file-type" +msgid "DXF file (AutoCAD 2007)" +msgstr "DXF dosyası (AutoCAD 2007)" + +#: platform/gui.cpp:117 +msgctxt "file-type" +msgid "HPGL file" +msgstr "HPGL dosyası" + +#: platform/gui.cpp:118 +msgctxt "file-type" +msgid "G Code" +msgstr "G Kodu" + +#: platform/gui.cpp:127 +msgctxt "file-type" +msgid "AutoCAD DXF and DWG files" +msgstr "AutoCAD DXF ve DWG dosyaları" + +#: platform/gui.cpp:131 +msgctxt "file-type" +msgid "Comma-separated values" +msgstr "Virgülle ayrılmış değerler (CSV)" + +#: platform/guigtk.cpp:1434 platform/guimac.mm:1513 platform/guiwin.cpp:1641 +msgid "untitled" +msgstr "isimsiz" + +#: platform/guigtk.cpp:1445 platform/guigtk.cpp:1481 platform/guimac.mm:1471 +#: platform/guiwin.cpp:1639 +msgctxt "title" +msgid "Save File" +msgstr "Dosyayı Kaydet" + +#: platform/guigtk.cpp:1446 platform/guigtk.cpp:1482 platform/guimac.mm:1454 +#: platform/guiwin.cpp:1645 +msgctxt "title" +msgid "Open File" +msgstr "Dosyayı Aç" + +#: platform/guigtk.cpp:1449 platform/guigtk.cpp:1488 +msgctxt "button" +msgid "_Cancel" +msgstr "_İptal" + +#: platform/guigtk.cpp:1450 platform/guigtk.cpp:1486 +msgctxt "button" +msgid "_Save" +msgstr "_Kaydet" + +#: platform/guigtk.cpp:1451 platform/guigtk.cpp:1487 +msgctxt "button" +msgid "_Open" +msgstr "_Aç" + +#: solvespace.cpp:175 +msgctxt "title" +msgid "Autosave Available" +msgstr "Otomatik Kaydetme Kullanılabilir" + +#: solvespace.cpp:176 +msgctxt "dialog" +msgid "An autosave file is available for this sketch." +msgstr "Bu çizim için otomatik kaydetme dosyası kullanılabilir." + +#: solvespace.cpp:177 +msgctxt "dialog" +msgid "Do you want to load the autosave file instead?" +msgstr "Bunun yerine otomatik kaydetme dosyasını yüklemek istiyor musunuz?" + +#: solvespace.cpp:178 +msgctxt "button" +msgid "&Load autosave" +msgstr "&Otomatik kaydı yükle" + +#: solvespace.cpp:180 +msgctxt "button" +msgid "Do&n't Load" +msgstr "&Yükleme" + +#: solvespace.cpp:640 +msgctxt "title" +msgid "Modified File" +msgstr "Değiştirilen Dosya" + +#: solvespace.cpp:642 +#, c-format +msgctxt "dialog" +msgid "Do you want to save the changes you made to the sketch “%s”?" +msgstr "\"%s\" çiziminde yaptığınız değişiklikleri kaydetmek istiyor musunuz?" + +#: solvespace.cpp:645 +msgctxt "dialog" +msgid "Do you want to save the changes you made to the new sketch?" +msgstr "Yeni çizimde yaptığınız değişiklikleri kaydetmek istiyor musunuz?" + +#: solvespace.cpp:648 +msgctxt "dialog" +msgid "Your changes will be lost if you don't save them." +msgstr "Bunları kaydetmezseniz değişiklikleriniz kaybolur." + +#: solvespace.cpp:649 +msgctxt "button" +msgid "&Save" +msgstr "&Kaydet" + +#: solvespace.cpp:651 +msgctxt "button" +msgid "Do&n't Save" +msgstr "K&aydetme" + +#: solvespace.cpp:672 +msgctxt "title" +msgid "(new sketch)" +msgstr "(yeni çizim)" + +#: solvespace.cpp:683 +msgctxt "title" +msgid "Property Browser" +msgstr "Özellik Penceresi" + +#: solvespace.cpp:746 +msgid "" +"Constraints are currently shown, and will be exported in the toolpath. This " +"is probably not what you want; hide them by clicking the link at the top of " +"the text window." +msgstr "" +"Kısıtlamalar şu anda gösterilmektedir ve takım yolunda dışa aktarılacaktır. " +"Muhtemelen istediğiniz bu değil; Özellik Penceresinin üst kısmındaki " +"bağlantıya tıklayarak bunları gizleyin." + +#: solvespace.cpp:834 +#, c-format +msgid "" +"Can't identify file type from file extension of filename '%s'; try .dxf or ." +"dwg." +msgstr "" +"'%s' dosya adının dosya uzantısından dosya türü belirlenemiyor; .dxf veya ." +"dwg'yi deneyin." + +#: solvespace.cpp:886 +msgid "Constraint must have a label, and must not be a reference dimension." +msgstr "Kısıtlamanın bir etiketi olmalı ve bir referans ölçüsü olmamalıdır." + +#: solvespace.cpp:890 +msgid "Bad selection for step dimension; select a constraint." +msgstr "Adım ölçüsü ayarlamak için hatalı seçim; bir kısıtlama seçin." + +#: solvespace.cpp:914 +msgid "The assembly does not interfere, good." +msgstr "Montajda engel oluşturan parça yok, iyi." + +#: solvespace.cpp:930 +#, c-format +msgid "" +"The volume of the solid model is:\n" +"\n" +" %s" +msgstr "" +"Katı modelin hacmi:\n" +"\n" +" % s" + +#: solvespace.cpp:939 +#, c-format +msgid "" +"\n" +"The volume of current group mesh is:\n" +"\n" +" %s" +msgstr "" +"\n" +"Mevcut ağ (kafes) grubunun hacmi:\n" +"\n" +" %s" + +#: solvespace.cpp:944 +msgid "" +"\n" +"\n" +"Curved surfaces have been approximated as triangles.\n" +"This introduces error, typically of around 1%." +msgstr "" +"\n" +"\n" +"Kavisli (Eğri) yüzeyler üçgenler olarak yaklaştırılmıştır.\n" +"Bu, genel olarak yaklaşık 1% hataya neden olur." + +#: solvespace.cpp:959 +#, c-format +msgid "" +"The surface area of the selected faces is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"Seçili yüzlerin yüzey alanı:\n" +"\n" +" %s\n" +"\n" +"Eğriler, doğrusal parçalı olarak yaklaştırılmıştır.\n" +"Bu, genel olarak yaklaşık 1%% hataya neden olur." + +#: solvespace.cpp:968 +msgid "" +"This group does not contain a correctly-formed 2d closed area. It is open, " +"not coplanar, or self-intersecting." +msgstr "" +"Bu grup, doğru biçimlendirilmiş 2d kapalı alan içermiyor. Açık, eş-düzlemli " +"değil veya kendisiyle kesişiyor." + +#: solvespace.cpp:980 +#, c-format +msgid "" +"The area of the region sketched in this group is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"Bu grupta çizilen bölgenin alanı:\n" +"\n" +" %s\n" +"\n" +"Eğriler, doğrusal parçalı olarak yaklaştırılmıştır.\n" +"Bu, genel olarak yaklaşık 1%% hataya neden olur." + +#: solvespace.cpp:1000 +#, c-format +msgid "" +"The total length of the selected entities is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"Seçilen öğelerin toplam uzunluğu:\n" +"\n" +" %s\n" +"\n" +"Eğriler, parçalı doğrusal olarak yaklaştırıldı.\n" +"Bu, genel olarak yaklaşık 1%% hataya neden olur." + +#: solvespace.cpp:1006 +msgid "Bad selection for perimeter; select line segments, arcs, and curves." +msgstr "" +"Çevre uzunluğunu hesabı için hatalı seçim; çizgi parçalarını, yayları ve " +"eğrileri seçin." + +#: solvespace.cpp:1022 +msgid "Bad selection for trace; select a single point." +msgstr "Nokta izleme işlemi için hatalı seçim; tek bir nokta seçin." + +#: solvespace.cpp:1049 +#, c-format +msgid "Couldn't write to '%s'" +msgstr "\"%s\" öğesine yazılamadı" + +#: solvespace.cpp:1079 +msgid "The mesh is self-intersecting (NOT okay, invalid)." +msgstr "Ağ (Kafes) kendisiyle kesişiyor (İyi DEĞİL, geçersiz)." + +#: solvespace.cpp:1080 +msgid "The mesh is not self-intersecting (okay, valid)." +msgstr "Ağ (Kafes) kendisi ile kesişmiyor (iyi, geçerli)." + +#: solvespace.cpp:1082 +msgid "The mesh has naked edges (NOT okay, invalid)." +msgstr "Ağın (Kafesin) açık kenarları var (iyi DEĞİL, geçersiz)." + +#: solvespace.cpp:1083 +msgid "The mesh is watertight (okay, valid)." +msgstr "Ağ (Kafes) çok sıkı (iyi, geçerli)" + +#: solvespace.cpp:1086 +#, c-format +msgid "" +"\n" +"\n" +"The model contains %d triangles, from %d surfaces." +msgstr "" +"\n" +"\n" +"Model, %d yüzeyden %d üçgen içeriyor." + +#: solvespace.cpp:1090 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"Zero problematic edges, good.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"Sıfır sorunlu kenar, iyi.%s" + +#: solvespace.cpp:1093 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d problematic edges, bad.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d sorunlu kenar, kötü.%s" + +#: solvespace.cpp:1106 +#, c-format +msgid "" +"This is SolveSpace version %s.\n" +"\n" +"For more information, see http://solvespace.com/\n" +"\n" +"SolveSpace is free software: you are free to modify\n" +"and/or redistribute it under the terms of the GNU\n" +"General Public License (GPL) version 3 or later.\n" +"\n" +"There is NO WARRANTY, to the extent permitted by\n" +"law. For details, visit http://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues and other authors.\n" +msgstr "" +"Bu SolveSpace'in %s sürümüdür.\n" +"\n" +"Daha fazla bilgi için bkz., http://solvespace.com/\n" +"\n" +"SolveSpace ücretsiz bir yazılımdır: GNU Genel Kamu \n" +"Lisansı (GPL) sürüm 3 veya daha sonraki hükümleri altında \n" +"onu değiştirmekte ve / veya yeniden dağıtmakta özgürsünüz.\n" +"\n" +"Lisans kuralları haricinde GARANTİ YOKTUR.\n" +"Ayrıntılar için http://gnu.org/licenses/ adresini ziyaret edin.\n" +"\n" +"© 2008-% d Jonathan Westhues ve diğer yazarlar.\n" + +#: style.cpp:185 +msgid "" +"Can't assign style to an entity that's derived from another entity; try " +"assigning a style to this entity's parent." +msgstr "" +"Başka bir öğeden türetilen bir öğeye biçim atayamazsınız; bu öğenin " +"üst öğesine bir biçim atamayı deneyin." + +#: style.cpp:735 +msgid "Style name cannot be empty" +msgstr "Biçim adı boş olamaz" + +#: textscreens.cpp:837 +msgid "Can't repeat fewer than 1 time." +msgstr "1 defadan az tekrarlanamaz." + +#: textscreens.cpp:841 +msgid "Can't repeat more than 999 times." +msgstr "999 defadan fazla tekrarlanamaz." + +#: textscreens.cpp:866 +msgid "Group name cannot be empty" +msgstr "Grup adı boş olamaz" + +#: textscreens.cpp:918 +msgid "Opacity must be between zero and one." +msgstr "Şeffaflık değeri sıfır ile bir arasında olmalıdır." + +#: textscreens.cpp:953 +msgid "Radius cannot be zero or negative." +msgstr "Yarıçap sıfır veya negatif değer olamaz." + +#: toolbar.cpp:18 +msgid "Sketch line segment" +msgstr "Çizgi parçası çiz" + +#: toolbar.cpp:20 +msgid "Sketch rectangle" +msgstr "Dikdörtgen çiz" + +#: toolbar.cpp:22 +msgid "Sketch circle" +msgstr "Daire çiz" + +#: toolbar.cpp:24 +msgid "Sketch arc of a circle" +msgstr "Bir çember yayı çiz" + +#: toolbar.cpp:26 +msgid "Sketch curves from text in a TrueType font" +msgstr "TrueType yazı tipindeki metinden eğriler çiz" + +#: toolbar.cpp:28 +msgid "Sketch image from a file" +msgstr "Bir dosyadan görüntü (resim) ekle" + +#: toolbar.cpp:30 +msgid "Create tangent arc at selected point" +msgstr "Seçilen noktada radyus (teğet yay) oluştur" + +#: toolbar.cpp:32 +msgid "Sketch cubic Bezier spline" +msgstr "Kübik Bezier Eğri Çiz" + +#: toolbar.cpp:34 +msgid "Sketch datum point" +msgstr "Referans Nokta Ekle" + +#: toolbar.cpp:36 +msgid "Toggle construction" +msgstr "Yapıyı değiştir" + +#: toolbar.cpp:38 +msgid "Split lines / curves where they intersect" +msgstr "Çizgileri / eğrileri kesiştikleri yerden böl" + +#: toolbar.cpp:42 +msgid "Constrain distance / diameter / length" +msgstr "Mesafeyi / çapı / uzunluğu kısıtla" + +#: toolbar.cpp:44 +msgid "Constrain angle" +msgstr "Açıyı kısıtla" + +#: toolbar.cpp:46 +msgid "Constrain to be horizontal" +msgstr "Yatay olarak kısıtla" + +#: toolbar.cpp:48 +msgid "Constrain to be vertical" +msgstr "Dikey olarak kısıtla" + +#: toolbar.cpp:50 +msgid "Constrain to be parallel or tangent" +msgstr "Paralel veya teğet olarak kısıtla" + +#: toolbar.cpp:52 +msgid "Constrain to be perpendicular" +msgstr "Dik olarak kısıtla" + +#: toolbar.cpp:54 +msgid "Constrain point on line / curve / plane / point" +msgstr "Noktayı çizgi / eğri / düzlem / nokta ile çakıştırarak kısıtla" + +#: toolbar.cpp:56 +msgid "Constrain symmetric" +msgstr "Simetrik olarak kısıtla" + +#: toolbar.cpp:58 +msgid "Constrain equal length / radius / angle" +msgstr "Eşit uzunluk / yarıçap / açı olarak kısıtla" + +#: toolbar.cpp:60 +msgid "Constrain normals in same orientation" +msgstr "Normalleri aynı yönde kısıtla" + +#: toolbar.cpp:62 +msgid "Other supplementary angle" +msgstr "Diğer bütünler açı" + +#: toolbar.cpp:64 +msgid "Toggle reference dimension" +msgstr "Ölçüyü Referans Yap / Yapma" + +#: toolbar.cpp:68 +msgid "New group extruding active sketch" +msgstr "Etkin çizimi uzatıp katılayarak yeni grup oluştur" + +#: toolbar.cpp:70 +msgid "New group rotating active sketch" +msgstr "Etkin çizimi tam döndürerek yeni grup oluştur" + +#: toolbar.cpp:72 +msgid "New group helix from active sketch" +msgstr "Etkin çizimden sarmal yeni grup oluştur" + +#: toolbar.cpp:74 +msgid "New group revolve active sketch" +msgstr "Etkin çizimi kısmen döndürerek yeni grup oluştur" + +#: toolbar.cpp:76 +msgid "New group step and repeat rotating" +msgstr "Adım ve tekrar değeri belirterek dairesel kopya grubu oluştur" + +#: toolbar.cpp:78 +msgid "New group step and repeat translating" +msgstr "Adım ve tekrar değeri belirterek yeni doğrusal kopyalama grup oluştur" + +#: toolbar.cpp:80 +msgid "New group in new workplane (thru given entities)" +msgstr "" +"Yeni çalışma düzleminde yeni grup oluştur (verilen öğeler aracılığıyla)" + +#: toolbar.cpp:82 +msgid "New group in 3d" +msgstr "3d'de yeni grup oluştur" + +#: toolbar.cpp:84 +msgid "New group linking / assembling file" +msgstr "Bağlantı / Montaj dosyası ile yeni grup oluştur" + +#: toolbar.cpp:88 +msgid "Nearest isometric view" +msgstr "En yakın izometrik görünüm" + +#: toolbar.cpp:90 +msgid "Align view to active workplane" +msgstr "Görünümü etkin çalışma düzlemine hizala" + +#: util.cpp:165 +msgctxt "title" +msgid "Error" +msgstr "Hata" + +#: util.cpp:165 +msgctxt "title" +msgid "Message" +msgstr "Mesaj" + +#: util.cpp:170 +msgctxt "button" +msgid "&OK" +msgstr "&Tamam" + +#: view.cpp:127 +msgid "Scale cannot be zero or negative." +msgstr "Ölçek sıfır veya negatif olamaz." + +#: view.cpp:139 view.cpp:148 +msgid "Bad format: specify x, y, z" +msgstr "Kötü biçim: x, y, z'yi belirtin" + + diff --git a/res/locales/uk_UA.po b/res/locales/uk_UA.po new file mode 100644 index 0000000..24ca895 --- /dev/null +++ b/res/locales/uk_UA.po @@ -0,0 +1,2329 @@ +# Ukrainian translations for SolveSpace package. +# Copyright (C) 2017 the SolveSpace authors +# This file is distributed under the same license as the SolveSpace package. +# app4soft , 2021. +msgid "" +msgstr "" +"Project-Id-Version: SolveSpace 3.0\n" +"Report-Msgid-Bugs-To: phkahler@gmail.com\n" +"POT-Creation-Date: 2025-01-26 21:04+0200\n" +"PO-Revision-Date: 2025-01-27 12:58+0200\n" +"Last-Translator: https://github.com/Symbian9\n" +"Language-Team: app4soft\n" +"Language: uk_UA\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.5\n" + +#: clipboard.cpp:314 +msgid "" +"Cut, paste, and copy work only in a workplane.\n" +"\n" +"Activate one with Sketch -> In Workplane." +msgstr "" +"Вирізання, вставка та копіювання працює лише у площині.\n" +"\n" +"Активуйте одну через Креслення -> У площині." + +#: clipboard.cpp:331 +msgid "Clipboard is empty; nothing to paste." +msgstr "Буфер обміну порожній; немає чого вставляти." + +#: clipboard.cpp:378 +msgid "Number of copies to paste must be at least one." +msgstr "Кількість копій для вставки має бути не менше одної." + +#: clipboard.cpp:394 textscreens.cpp:879 +msgid "Scale cannot be zero." +msgstr "Масштаб не може бути нульовим." + +#: clipboard.cpp:436 +msgid "Select one point to define origin of rotation." +msgstr "Оберіть одну точку для визначення центру обертання." + +#: clipboard.cpp:448 +msgid "Select two points to define translation vector." +msgstr "Оберіть дві точки для визначення вектору розміщення." + +#: clipboard.cpp:458 +msgid "" +"Transformation is identity. So all copies will be exactly on top of each " +"other." +msgstr "Трансформація ідентична, тому усі копії будуть точно одна на одній." + +#: clipboard.cpp:462 +msgid "Too many items to paste; split this into smaller pastes." +msgstr "" +"Забагато об'єктів для вставки; розділіть копіювання на кілька менших етапів." + +#: clipboard.cpp:467 +msgid "No workplane active." +msgstr "Немає активної площини." + +#: confscreen.cpp:410 +msgid "Bad format: specify coordinates as x, y, z" +msgstr "Некоректний формат: визначте координати як X, Y, Z" + +#: confscreen.cpp:420 style.cpp:729 textscreens.cpp:910 +msgid "Bad format: specify color as r, g, b" +msgstr "Некоректний формат: визначте колір як R, G, B" + +#: confscreen.cpp:446 +msgid "" +"The perspective factor will have no effect until you enable View -> Use " +"Perspective Projection." +msgstr "" +"Значення перспективи не матиме ефекту допоки не ввімкнено Вигляд -> " +"Використовувати Перспективну Проєкцію." + +#: confscreen.cpp:464 confscreen.cpp:474 +#, c-format +msgid "Specify between 0 and %d digits after the decimal." +msgstr "Визначте кількість десяткових знаків від 0 до %d." + +#: confscreen.cpp:486 +msgid "Export scale must not be zero!" +msgstr "Масштаб експорту не може бути нульовим!" + +#: confscreen.cpp:498 +msgid "Cutter radius offset must not be negative!" +msgstr "Радіус відступу різання не може бути від'ємним!" + +#: confscreen.cpp:557 +msgid "Bad value: autosave interval should be positive" +msgstr "Некоректне значення: інтервал автозбереження має бути додатнім" + +#: confscreen.cpp:560 +msgid "Bad format: specify interval in integral minutes" +msgstr "Некоректний формат: визначте інтервал цілим числом у хвилинах" + +#: constraint.cpp:12 +msgctxt "constr-name" +msgid "pts-coincident" +msgstr "співпадіння-тчк" + +#: constraint.cpp:13 +msgctxt "constr-name" +msgid "pt-pt-distance" +msgstr "відстань-тчк-тчк" + +#: constraint.cpp:14 +msgctxt "constr-name" +msgid "pt-line-distance" +msgstr "відстань-тчк-лінія" + +#: constraint.cpp:15 +msgctxt "constr-name" +msgid "pt-plane-distance" +msgstr "відстань-тчк-площина" + +#: constraint.cpp:16 +msgctxt "constr-name" +msgid "pt-face-distance" +msgstr "відстань-тчк-грань" + +#: constraint.cpp:17 +msgctxt "constr-name" +msgid "proj-pt-pt-distance" +msgstr "проєкційна-відстань-тчк-тчк" + +#: constraint.cpp:18 +msgctxt "constr-name" +msgid "pt-in-plane" +msgstr "тчк-у-площині" + +#: constraint.cpp:19 +msgctxt "constr-name" +msgid "pt-on-line" +msgstr "тчк-на-лінії" + +#: constraint.cpp:20 +msgctxt "constr-name" +msgid "pt-on-face" +msgstr "тчк-на-грані" + +#: constraint.cpp:21 +msgctxt "constr-name" +msgid "eq-length" +msgstr "рівні-довжини" + +#: constraint.cpp:22 +msgctxt "constr-name" +msgid "eq-length-and-pt-ln-dist" +msgstr "рівні-довжина-відстань-тчк-лінія" + +#: constraint.cpp:23 +msgctxt "constr-name" +msgid "eq-pt-line-distances" +msgstr "рівна-відстань-тчк-лінія" + +#: constraint.cpp:24 +msgctxt "constr-name" +msgid "length-ratio" +msgstr "пропорція-довжин" + +#: constraint.cpp:25 +msgctxt "constr-name" +msgid "arc-arc-length-ratio" +msgstr "пропорція-довжин-дуга-дуга" + +#: constraint.cpp:26 +msgctxt "constr-name" +msgid "arc-line-length-ratio" +msgstr "пропорція-довжини-дуга-лінія" + +#: constraint.cpp:27 +msgctxt "constr-name" +msgid "length-difference" +msgstr "різниця-довжин" + +#: constraint.cpp:28 +msgctxt "constr-name" +msgid "arc-arc-len-difference" +msgstr "різниця-довжин-дуга-дуга" + +#: constraint.cpp:29 +msgctxt "constr-name" +msgid "arc-line-len-difference" +msgstr "різниця-довжин-дуга-лінія" + +#: constraint.cpp:30 +msgctxt "constr-name" +msgid "symmetric" +msgstr "симетрія" + +#: constraint.cpp:31 +msgctxt "constr-name" +msgid "symmetric-h" +msgstr "симетрія-вертикально" + +#: constraint.cpp:32 +msgctxt "constr-name" +msgid "symmetric-v" +msgstr "симетрія-горизонтально" + +#: constraint.cpp:33 +msgctxt "constr-name" +msgid "symmetric-line" +msgstr "симетрія-відносно-лінії" + +#: constraint.cpp:34 +msgctxt "constr-name" +msgid "at-midpoint" +msgstr "на-середині" + +#: constraint.cpp:35 +msgctxt "constr-name" +msgid "horizontal" +msgstr "горизонталь" + +#: constraint.cpp:36 +msgctxt "constr-name" +msgid "vertical" +msgstr "вертикаль" + +#: constraint.cpp:37 +msgctxt "constr-name" +msgid "diameter" +msgstr "діаметр" + +#: constraint.cpp:38 +msgctxt "constr-name" +msgid "pt-on-circle" +msgstr "тчк-на-колі" + +#: constraint.cpp:39 +msgctxt "constr-name" +msgid "same-orientation" +msgstr "співнапрямленість" + +#: constraint.cpp:40 +msgctxt "constr-name" +msgid "angle" +msgstr "кут" + +#: constraint.cpp:41 +msgctxt "constr-name" +msgid "parallel" +msgstr "паралель" + +#: constraint.cpp:42 +msgctxt "constr-name" +msgid "arc-line-tangent" +msgstr "дотичні-дуга-лінія" + +#: constraint.cpp:43 +msgctxt "constr-name" +msgid "cubic-line-tangent" +msgstr "дотичні-сплайн-лінія" + +#: constraint.cpp:44 +msgctxt "constr-name" +msgid "curve-curve-tangent" +msgstr "дотичні-крива-крива" + +#: constraint.cpp:45 +msgctxt "constr-name" +msgid "perpendicular" +msgstr "перпендикуляр" + +#: constraint.cpp:46 +msgctxt "constr-name" +msgid "eq-radius" +msgstr "рівнозначні-радіуси" + +#: constraint.cpp:47 +msgctxt "constr-name" +msgid "eq-angle" +msgstr "рівнозначні-кути" + +#: constraint.cpp:48 +msgctxt "constr-name" +msgid "eq-line-len-arc-len" +msgstr "рівнозначні-лінія-дуга" + +#: constraint.cpp:49 +msgctxt "constr-name" +msgid "lock-where-dragged" +msgstr "фіксоване-положення" + +#: constraint.cpp:50 +msgctxt "constr-name" +msgid "comment" +msgstr "коментар" + +#: constraint.cpp:151 +msgid "" +"The point you selected does not belong to the arc. The arc and line segment " +"do not share an end point.\n" +"\n" +"Select the end point of the arc at which you want it to be tangent to the " +"line." +msgstr "" +"Обрана вами точка не належить до дуги. Дуга та відрізок не мають спільної " +"кінцевої точки.\n" +"\n" +"Оберіть кінцеву точку дуги, в якій ви хочете зробити дугу дотичною до лінії." + +#: constraint.cpp:158 +msgid "" +"The tangent arc and line segment must share an endpoint. Constrain them with " +"Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the arc at which you want it to be " +"tangent to the line." +msgstr "" +"Дотичні дуга та лінія повинні мати спільну кінцеву точку. Використайте " +"Обмежити -> На точці перед застосуванням дотичного обмеження.\n" +"\n" +"Або оберіть кінцеву точку дуги в якій ви хочете зробити її дотичною до лінії." + +#: constraint.cpp:186 +msgid "" +"The point you selected is not an end point of the cubic spline. The spline " +"and line segment do not share an end point.\n" +"\n" +"Select the end point of the spline at which you want it to be tangent to the " +"line." +msgstr "" +"Обрана вами точка не є кінцевою точкою кубічного сплайну. Сплайн та відрізок " +"не мають спільної кінцевої точки.\n" +"\n" +"Оберіть кінцеву точку сплайну в якій ви хочете зробити його дотичним до " +"лінії." + +#: constraint.cpp:193 +msgid "" +"The tangent cubic spline and line segment must share an endpoint. Constrain " +"them with Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the cubic spline at which you want it " +"to be tangent to the line." +msgstr "" +"Дотичні сплайн та відрізок повинні мати спільну кінцеву точку. Використайте " +"Обмежити -> На точці перед застосуванням дотичного обмеження.\n" +"\n" +"Або оберіть кінцеву точку сплайну в якій ви хочете зробити його дотичним до " +"лінії." + +#: constraint.cpp:237 +msgid "" +"The points you selected are not end points of the two curves. The curves do " +"not share an end point.\n" +"\n" +"Select the end points of both curves at which you want them to be tangent to " +"each other." +msgstr "" +"Обрані вами точки не є кінцевими точками двох дуг. Дуги не поділяють кінцеву " +"точку.\n" +"\n" +"Оберіть кінцеві точки обох дуг, в яких ви бажаєте зробити їх дотичними одна " +"одній." + +#: constraint.cpp:244 +msgid "" +"The curves must share an endpoint. Constrain them with Constrain -> On Point " +"before constraining tangent.\n" +"\n" +"Alternatively select the end points of both curves at which you want the " +"curves to be tangent." +msgstr "" +"Дотичні дуги повинні мати спільну кінцеву точку. Використайте Обмежити -> На " +"точці перед застосуванням дотичного обмеження.\n" +"\n" +"Або оберіть кінцеві точки обох дуг в яких ви хочете зробити дуги дотичними." + +#: constraint.cpp:303 +msgid "" +"Bad selection for distance / diameter constraint. This constraint can apply " +"to:\n" +"\n" +" * two points (distance between points)\n" +" * a line segment (length)\n" +" * two points and a line segment or normal (projected distance)\n" +" * a workplane and a point (minimum distance)\n" +" * a line segment and a point (minimum distance)\n" +" * a plane face and a point (minimum distance)\n" +" * a circle or an arc (diameter)\n" +msgstr "" +"Поганий вибір для обмеження за Відстанню / Діаметром. Це обмеження можна " +"застосувати до:\n" +"\n" +" * двох точок (відстань між точками)\n" +" * відрізку прямої (довжина)\n" +" * двох точок та відрізку прямої чи нормалі (відстань між проєкціями " +"точок)\n" +" * робочої площини та точки (найменша відстань)\n" +" * відрізку прямої та точки (найменша відстань)\n" +" * грані та точки (найменша відстань)\n" +" * кола або дуги (діаметр)\n" + +#: constraint.cpp:366 +msgid "" +"Bad selection for on point / curve / plane constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points (points coincident)\n" +" * a point and a workplane (point in plane)\n" +" * a point and a line segment (point on line)\n" +" * a point and a circle or arc (point on curve)\n" +" * a point and one to three plane faces (point on face(s))\n" +msgstr "" +"Поганий вибір для обмеження На Точці / Кривій / Площині. Це обмеження можна " +"застосувати до:\n" +"\n" +" * двох або більше точок (точки збігаються)\n" +" * точки та площини (точка лежить в площині)\n" +" * точки та відрізку прямої (точка лежить на прямій)\n" +" * точки та кола чи дуги (точка лежить на дузі)\n" +" * точки та від одієї до трьох граней (точка лежить на грані(ях))\n" + +#: constraint.cpp:427 +msgid "" +"Bad selection for equal length / radius constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more line segments (equal length)\n" +" * two line segments and two points (equal point-line distances)\n" +" * a line segment and two points (equal point-line distances)\n" +" * a line segment, and a point and line segment (point-line distance " +"equals length)\n" +" * two or more circles or arcs (equal radius)\n" +" * a line segment and an arc (line segment length equals arc length)\n" +msgstr "" +"Поганий вибір для обмеження Рівні Довжина / Радіус. Це обмеження можна " +"застосувати до:\n" +"\n" +" * двох або більше відрізків прямої (рівні довжини)\n" +" * двох відрізків та двох точок (рівні відстані від точок до відповідних " +"відрізків)\n" +" * відрізка та двох точок (рівні відстані від точок до відрізка)\n" +" * відрізка, точки, та другого відрізка (відстань від точки до другого " +"відрізка дорівнює першому)\n" +" * двох або більше кіл чи дуг (рівні радіуси)\n" +" * відрізку та дуги (довжина відрізку дорівнює довжині дуги) \n" + +#: constraint.cpp:480 +msgid "" +"Bad selection for length ratio constraint. This constraint can apply to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"Поганий вибір для обмеження Пропорція Довжин. Це обмеження можна застосувати " +"до:\n" +"\n" +" * двох відрізків прямої\n" +" * двох дуг\n" +" * дуги та відрізка прямої\n" + +#: constraint.cpp:515 +msgid "" +"Bad selection for length difference constraint. This constraint can apply " +"to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"Поганий вибір для обмеження Різниця Довжин. Це обмеження можна застосувати " +"до:\n" +"\n" +" * двох відрізків\n" +" * двох дуг\n" +" * відрізка та дуги\n" + +#: constraint.cpp:550 +msgid "" +"Bad selection for at midpoint constraint. This constraint can apply to:\n" +"\n" +" * a line segment and a point (point at midpoint)\n" +" * a line segment and a workplane (line's midpoint on plane)\n" +msgstr "" +"Поганий вибір для обмеження До Середини. Це обмеження можна застосувати до:\n" +"\n" +" * відрізка та точки (точка на середині відрізку)\n" +" * відрізка та робочої площини (середина відрізку лежить в площині)\n" + +#: constraint.cpp:608 +msgid "" +"Bad selection for symmetric constraint. This constraint can apply to:\n" +"\n" +" * two points or a line segment (symmetric about workplane's coordinate " +"axis)\n" +" * line segment, and two points or a line segment (symmetric about line " +"segment)\n" +" * workplane, and two points or a line segment (symmetric about " +"workplane)\n" +msgstr "" +"Поганий вибір для обмеження за симетрією. Це обмеження можна застосувати " +"до:\n" +"\n" +" * двох точок або відрізку прямої (симетрія відносно початку координат " +"робочої площини)\n" +" * відрізка та двох точок або іншого відрізка (симетрія відносно " +"відрізка)\n" +" * робочої площини та двох точок або відрізка (симетрія відносно робочої " +"площини)\n" + +#: constraint.cpp:623 +msgid "" +"A workplane must be active when constraining symmetric without an explicit " +"symmetry plane." +msgstr "" +"Робоча площина має бути активною коли використовується обмеження за " +"симетрією без явного вибору площини." + +#: constraint.cpp:663 +msgid "" +"Activate a workplane (with Sketch -> In Workplane) before applying a " +"horizontal or vertical constraint." +msgstr "" +"Активуйте площину (в Креслення -> У Робочій Площині) перед застосуванням " +"обмеження Горизонтально або Вертикально." + +#: constraint.cpp:679 +msgid "" +"Bad selection for horizontal / vertical constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points\n" +" * one or more line segments\n" +msgstr "" +"Поганий вибір для обмеження Горизонтально / Вертикально. Це обмеження можна " +"застосувати до:\n" +"\n" +" * двох або більше точок\n" +" * одного або більше відрізків\n" + +#: constraint.cpp:697 +msgid "" +"Bad selection for same orientation constraint. This constraint can apply " +"to:\n" +"\n" +" * two normals\n" +msgstr "" +"Поганий вибір для обмеження Однакова Орієнтація. Це обмеження можна " +"застосувати до:\n" +"\n" +" * двох перпендикулярів\n" + +#: constraint.cpp:748 +msgid "Must select an angle constraint." +msgstr "Необхідно обрати кут." + +#: constraint.cpp:761 +msgid "Must select a constraint with associated label." +msgstr "Необхідно обрати обмежувач з відповідною міткою." + +#: constraint.cpp:784 +msgid "" +"Bad selection for angle constraint. This constraint can apply to:\n" +"\n" +"Angle between:\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +"\n" +"Equal angles:\n" +" * four line segments or normals (equal angle between A,B and C,D)\n" +" * three line segments or normals (equal angle between A,B and B,C)\n" +msgstr "" +"Поганий вибір для обмеження за Кутом. Це обмеження можна застосувати до:\n" +"\n" +"Кута між:\n" +" * двома відрізками\n" +" * відрізком та перпендикуляром\n" +" * двома перпендикулярами\n" +"\n" +"Рівних кутів між:\n" +" * чотирма відрізками чи перпендикулярами (рівні кути між А,Б та В,Г)\n" +" * трьома відрізками чи перпендикулярами (рівні кути між А,Б та Б,В)\n" + +#: constraint.cpp:872 +msgid "Curve-curve tangency must apply in workplane." +msgstr "Обмеження дотичності дуг має відбуватися у робочій площині." + +#: constraint.cpp:887 +msgid "" +"Bad selection for parallel / tangent constraint. This constraint can apply " +"to:\n" +"\n" +" * two faces\n" +" * two or more line segments (parallel)\n" +" * one or more line segments and one or more normals (parallel)\n" +" * two or more normals (parallel)\n" +" * two line segments, arcs, or beziers, that share an endpoint (tangent)\n" +" * two line segments, arcs, or beziers, that do not share an endpoint and " +"the end point(s) of the curve(s) (tangent)\n" +msgstr "" +"Поганий вибір для обмеження Паралельно / Дотична. Це обмеження можна " +"застосувати до:\n" +"\n" +" * двох граней\n" +" * двох або більше відрізків (паралельно)\n" +" * одного або більше відрізка та одного або більше перпендикуляра " +"(паралельно)\n" +" * двох або більше перпендикулярів (паралельно)\n" +" * двох відрізків, дуг, чи кривих Безьє, що мають спільну кінцеву " +"точку(дотична)\n" +" * двох відрізків, дуг, чи кривих Безьє, що не мають спільної кінцевої " +"точки та кінцевою точкою(точками) дуги(дуг) (дотична)\n" + +#: constraint.cpp:914 +msgid "" +"Bad selection for perpendicular constraint. This constraint can apply to:\n" +"\n" +" * two faces\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +msgstr "" +"Поганий вибір для обмеження Перпендикулярно. Це обмеження можна застосувати " +"до:\n" +"\n" +" * двох граней\n" +" * двох відрізків\n" +" * відрізка та перпендикуляра\n" +" * двох перпендикулярів\n" + +#: constraint.cpp:931 +msgid "" +"Bad selection for lock point where dragged constraint. This constraint can " +"apply to:\n" +"\n" +" * a point\n" +msgstr "" +"Поганий вибір для обмеження Фіксувати Точку Після Переміщення. Це обмеження " +"можна застосувати до:\n" +"\n" +" * точки\n" + +#: constraint.cpp:946 mouse.cpp:1160 +msgid "NEW COMMENT -- DOUBLE-CLICK TO EDIT" +msgstr "КОМЕНТАР -- ДВІЧІ КЛІКНІТЬ ДЛЯ РЕДАГУВАННЯ" + +#: constraint.cpp:952 +msgid "click center of comment text" +msgstr "клікніть в місце де буде центр коментаря" + +#: export.cpp:19 +msgid "" +"No solid model present; draw one with extrudes and revolves, or use Export " +"2d View to export bare lines and curves." +msgstr "" +"Вісутня об'ємна модель; створіть модель за допомогою екструдування та " +"обертання або скористайтеся функцією \"Експортувати 2D Вигляд\" для еспорту " +"лише ліній та кривих." + +#: export.cpp:61 +msgid "" +"Bad selection for export section. Please select:\n" +"\n" +" * nothing, with an active workplane (workplane is section plane)\n" +" * a face (section plane through face)\n" +" * a point and two line segments (plane through point and parallel to " +"lines)\n" +msgstr "" +"Поганий вибір для експорту розрізу. Будьласка, оберіть:\n" +"\n" +" * нічого, з активною робочою площиною (робоча площина буде площиною " +"розрізу)\n" +" * грань (площина розрізу через грань)\n" +" * точку та два відрізки (площина розрізу через точку та паралельно " +"відрізкам)\n" + +#: export.cpp:818 +msgid "Active group mesh is empty; nothing to export." +msgstr "Активна група не містить сітку; немає чого експортувати." + +#: exportvector.cpp:336 +msgid "freehand lines were replaced with continuous lines" +msgstr "довільні лінії було замінено на неперервні відрізки" + +#: exportvector.cpp:338 +msgid "zigzag lines were replaced with continuous lines" +msgstr "zigzag лінії було замінено на неперервні відрізки" + +#: exportvector.cpp:592 +msgid "" +"Some aspects of the drawing have no DXF equivalent and were not exported:\n" +msgstr "" +"Деякі аспекти креслення на мають відповідників у форматі DXF і не будуть " +"експортовані:\n" + +#: exportvector.cpp:838 +msgid "" +"PDF page size exceeds 200 by 200 inches; many viewers may reject this file." +msgstr "" +"Розмір аркуша у PDF перевищує 200×200 дюймів; багато переглядачів можуть не " +"підтримувати цей файл." + +#: file.cpp:44 group.cpp:91 +msgctxt "group-name" +msgid "sketch-in-plane" +msgstr "креслення-в-площині" + +#: file.cpp:62 +msgctxt "group-name" +msgid "#references" +msgstr "#базові-площини" + +#: file.cpp:555 +msgid "The file is empty. It may be corrupt." +msgstr "Файл порожній. Він може бути пошкодженим." + +#: file.cpp:560 +msgid "" +"Unrecognized data in file. This file may be corrupt, or from a newer version " +"of the program." +msgstr "" +"Нерозпізнані дані у файлі. Цей файл може бути пошкодженим або збереженим " +"новою версією програми." + +#: file.cpp:876 +msgctxt "title" +msgid "Missing File" +msgstr "Втрачений Файл" + +#: file.cpp:877 +#, c-format +msgctxt "dialog" +msgid "The linked file “%s” is not present." +msgstr "Приєднаний файл “%s” відсутній." + +#: file.cpp:879 +msgctxt "dialog" +msgid "" +"Do you want to locate it manually?\n" +"\n" +"If you decline, any geometry that depends on the missing file will be " +"permanently removed." +msgstr "" +"Шукати файл вручну?\n" +"\n" +"При відмові, уся геометрія, що залежить від втраченого файлу буде видалена." + +#: file.cpp:882 +msgctxt "button" +msgid "&Yes" +msgstr "&Так" + +#: file.cpp:884 +msgctxt "button" +msgid "&No" +msgstr "&Ні" + +#: file.cpp:886 solvespace.cpp:652 +msgctxt "button" +msgid "&Cancel" +msgstr "&Відмінити" + +#: graphicswin.cpp:41 +msgid "&File" +msgstr "&Файл" + +#: graphicswin.cpp:42 +msgid "&New" +msgstr "&Новий" + +#: graphicswin.cpp:43 +msgid "&Open..." +msgstr "&Відкрити..." + +#: graphicswin.cpp:44 +msgid "Open &Recent" +msgstr "Відкрити &Недавні" + +#: graphicswin.cpp:45 +msgid "&Save" +msgstr "&Зберегти" + +#: graphicswin.cpp:46 +msgid "Save &As..." +msgstr "Зберегти &Як..." + +#: graphicswin.cpp:48 +msgid "Export &Image..." +msgstr "Експортувати &Зображення..." + +#: graphicswin.cpp:49 +msgid "Export 2d &View..." +msgstr "Експортувати 2D &Вигляд..." + +#: graphicswin.cpp:50 +msgid "Export 2d &Section..." +msgstr "Експортувати 2D &Секцію..." + +#: graphicswin.cpp:51 +msgid "Export 3d &Wireframe..." +msgstr "Експортувати 3D &Скелет..." + +#: graphicswin.cpp:52 +msgid "Export Triangle &Mesh..." +msgstr "Експортувати Тригранний &Каркас..." + +#: graphicswin.cpp:53 +msgid "Export &Surfaces..." +msgstr "Експортувати &Поверхні..." + +#: graphicswin.cpp:54 +msgid "Im&port..." +msgstr "Ім&портувати..." + +#: graphicswin.cpp:57 +msgid "E&xit" +msgstr "В&ихід" + +#: graphicswin.cpp:60 +msgid "&Edit" +msgstr "&Редагувати" + +#: graphicswin.cpp:61 +msgid "&Undo" +msgstr "&Відмінити" + +#: graphicswin.cpp:62 +msgid "&Redo" +msgstr "&Повторити" + +#: graphicswin.cpp:63 +msgid "Re&generate All" +msgstr "Ре&генерувати Все" + +#: graphicswin.cpp:65 +msgid "Snap Selection to &Grid" +msgstr "Прикріпити Виділене до &Сітки" + +#: graphicswin.cpp:66 +msgid "Rotate Imported &90°" +msgstr "Обернути Імпортоване на &90°" + +#: graphicswin.cpp:68 +msgid "Cu&t" +msgstr "Ви&різати" + +#: graphicswin.cpp:69 +msgid "&Copy" +msgstr "&Копіювати" + +#: graphicswin.cpp:70 +msgid "&Paste" +msgstr "&Вставити" + +#: graphicswin.cpp:71 +msgid "Paste &Transformed..." +msgstr "Вставити &Трансфмованим..." + +#: graphicswin.cpp:72 +msgid "&Delete" +msgstr "&Видалити" + +#: graphicswin.cpp:74 +msgid "Select &Edge Chain" +msgstr "Виділити Ланцуг &Ребер" + +#: graphicswin.cpp:75 +msgid "Select &All" +msgstr "Виділити &Усе" + +#: graphicswin.cpp:76 +msgid "&Unselect All" +msgstr "&Зняти Виділення з Усього" + +#: graphicswin.cpp:78 +msgid "&Line Styles..." +msgstr "Стилі &Ліній..." + +#: graphicswin.cpp:79 +msgid "&View Projection..." +msgstr "&Проекція Відображення..." + +#: graphicswin.cpp:81 +msgid "Con&figuration..." +msgstr "&Налаштування..." + +#: graphicswin.cpp:84 +msgid "&View" +msgstr "&Відображення" + +#: graphicswin.cpp:85 +msgid "Zoom &In" +msgstr "На&близити" + +#: graphicswin.cpp:86 +msgid "Zoom &Out" +msgstr "Від&далити" + +#: graphicswin.cpp:87 +msgid "Zoom To &Fit" +msgstr "Умістити на &Екрані" + +#: graphicswin.cpp:89 +msgid "Align View to &Workplane" +msgstr "Вирівняти Вигляд до Робочої &Площини" + +#: graphicswin.cpp:90 +msgid "Nearest &Ortho View" +msgstr "Найближчий &Ортогональний Вигляд" + +#: graphicswin.cpp:91 +msgid "Nearest &Isometric View" +msgstr "Найближчий &Ізометричний Вигляд" + +#: graphicswin.cpp:92 +msgid "&Center View At Point" +msgstr "&Центрувати Вигляд на Точці" + +#: graphicswin.cpp:94 +msgid "Show Snap &Grid" +msgstr "Показати &Сітку Прикріплення" + +#: graphicswin.cpp:95 +msgid "Darken Inactive Solids" +msgstr "Затінювати Неактивні Тіла" + +#: graphicswin.cpp:96 +msgid "Use &Perspective Projection" +msgstr "Використовувати &Перспективну Проекцію" + +#: graphicswin.cpp:97 +msgid "Show E&xploded View" +msgstr "Показати С&кладальне Креслення" + +#: graphicswin.cpp:98 +msgid "Dimension &Units" +msgstr "Розмірні &Одиниці" + +#: graphicswin.cpp:99 +msgid "Dimensions in &Millimeters" +msgstr "Розміри у &Міліметрах" + +#: graphicswin.cpp:100 +msgid "Dimensions in M&eters" +msgstr "Розміри у &Метрах" + +#: graphicswin.cpp:101 +msgid "Dimensions in &Inches" +msgstr "Розміри у &Дюймах" + +#: graphicswin.cpp:102 +msgid "Dimensions in &Feet and Inches" +msgstr "Розміри в &Футах та Дюймах" + +#: graphicswin.cpp:104 +msgid "Show &Toolbar" +msgstr "Показати Панель &Інструментів" + +#: graphicswin.cpp:105 +msgid "Show Property Bro&wser" +msgstr "Показати Вікно Власти&востей" + +#: graphicswin.cpp:107 +msgid "&Full Screen" +msgstr "&Повний Екран" + +#: graphicswin.cpp:109 +msgid "&New Group" +msgstr "&Нова Група" + +#: graphicswin.cpp:110 +msgid "Sketch In &3d" +msgstr "Креслення у &3D" + +#: graphicswin.cpp:111 +msgid "Sketch In New &Workplane" +msgstr "Креслення у Новій &Площині" + +#: graphicswin.cpp:113 +msgid "Step &Translating" +msgstr "Покрокове &Переміщення" + +#: graphicswin.cpp:114 +msgid "Step &Rotating" +msgstr "Покрокове &Обертання" + +#: graphicswin.cpp:116 +msgid "E&xtrude" +msgstr "Ви&давити" + +#: graphicswin.cpp:117 +msgid "&Helix" +msgstr "&Спіраль" + +#: graphicswin.cpp:118 +msgid "&Lathe" +msgstr "&Виточити" + +#: graphicswin.cpp:119 +msgid "Re&volve" +msgstr "&Обертати" + +#: graphicswin.cpp:121 +msgid "Link / Assemble..." +msgstr "Приєднати / Зібрати..." + +#: graphicswin.cpp:122 +msgid "Link Recent" +msgstr "Приєднати Нещодавні" + +#: graphicswin.cpp:124 +msgid "&Sketch" +msgstr "&Креслення" + +#: graphicswin.cpp:125 +msgid "In &Workplane" +msgstr "У Робочій &Площині" + +#: graphicswin.cpp:126 +msgid "Anywhere In &3d" +msgstr "Будь-де в &3D" + +#: graphicswin.cpp:128 +msgid "Datum &Point" +msgstr "Опорна &Точка" + +#: graphicswin.cpp:129 +msgid "Wor&kplane" +msgstr "Робоча &Площина" + +#: graphicswin.cpp:131 +msgid "Line &Segment" +msgstr "&Відрізок Прямої" + +#: graphicswin.cpp:132 +msgid "C&onstruction Line Segment" +msgstr "Контсрук&ційний Відрізок Прямої" + +#: graphicswin.cpp:133 +msgid "&Rectangle" +msgstr "&Прямокутник" + +#: graphicswin.cpp:134 +msgid "&Circle" +msgstr "&Коло" + +#: graphicswin.cpp:135 +msgid "&Arc of a Circle" +msgstr "&Дуга Кола" + +#: graphicswin.cpp:136 +msgid "&Bezier Cubic Spline" +msgstr "Кубічний Сплайн &Без'є" + +#: graphicswin.cpp:138 +msgid "&Text in TrueType Font" +msgstr "&Текст із TrueType Шрифтом" + +#: graphicswin.cpp:139 +msgid "I&mage" +msgstr "&Зображення" + +#: graphicswin.cpp:141 +msgid "To&ggle Construction" +msgstr "Пере&мкнути Конструктивність" + +#: graphicswin.cpp:142 +msgid "Ta&ngent Arc at Point" +msgstr "Дотична &Дуга на Точці" + +#: graphicswin.cpp:143 +msgid "Split Curves at &Intersection" +msgstr "Розрізати Криві на &Перетині" + +#: graphicswin.cpp:145 +msgid "&Constrain" +msgstr "&Обмежити" + +#: graphicswin.cpp:146 +msgid "&Distance / Diameter" +msgstr "&Відстань / Діаметр" + +#: graphicswin.cpp:147 +msgid "Re&ference Dimension" +msgstr "Від&носний Розмір" + +#: graphicswin.cpp:148 +msgid "A&ngle / Equal Angle" +msgstr "К&ут / Рівний Кут" + +#: graphicswin.cpp:149 +msgid "Reference An&gle" +msgstr "Відносний К&ут" + +#: graphicswin.cpp:150 +msgid "Other S&upplementary Angle" +msgstr "Інший Су&міжний Кут" + +#: graphicswin.cpp:151 +msgid "Toggle R&eference Dim" +msgstr "Перемкнути Від&носність Розмірів" + +#: graphicswin.cpp:153 +msgid "&Horizontal" +msgstr "&Горизонтально" + +#: graphicswin.cpp:154 +msgid "&Vertical" +msgstr "&Вертикально" + +#: graphicswin.cpp:156 +msgid "&On Point / Curve / Plane" +msgstr "&На точці / Кривій / Площині" + +#: graphicswin.cpp:157 +msgid "E&qual Length / Radius" +msgstr "Р&івна Довжина / Радіус" + +#: graphicswin.cpp:158 +msgid "Length / Arc Ra&tio" +msgstr "Пропорція довжин" + +#: graphicswin.cpp:159 +msgid "Length / Arc Diff&erence" +msgstr "Різниця довжин" + +#: graphicswin.cpp:160 +msgid "At &Midpoint" +msgstr "До &Середини" + +#: graphicswin.cpp:161 +msgid "S&ymmetric" +msgstr "Си&метрично" + +#: graphicswin.cpp:162 +msgid "Para&llel / Tangent" +msgstr "Пара&лельно / Дотична" + +#: graphicswin.cpp:163 +msgid "&Perpendicular" +msgstr "&Препендикулярно" + +#: graphicswin.cpp:164 +msgid "Same Orient&ation" +msgstr "Однакова Орієн&тація" + +#: graphicswin.cpp:165 +msgid "Lock Point Where &Dragged" +msgstr "Фіксувати Точку Після &Переміщення" + +#: graphicswin.cpp:167 +msgid "Comment" +msgstr "Коментар" + +#: graphicswin.cpp:169 +msgid "&Analyze" +msgstr "&Аналізувати" + +#: graphicswin.cpp:170 +msgid "Measure &Volume" +msgstr "Обрахувати &Об'єм" + +#: graphicswin.cpp:171 +msgid "Measure A&rea" +msgstr "Обрахувати Пл&ощу" + +#: graphicswin.cpp:172 +msgid "Measure &Perimeter" +msgstr "Обрахувати &Периметр" + +#: graphicswin.cpp:173 +msgid "Show &Interfering Parts" +msgstr "Показати &Дотичні Частини" + +#: graphicswin.cpp:174 +msgid "Show &Naked Edges" +msgstr "Показати &Приховані Ребра" + +#: graphicswin.cpp:175 +msgid "Show &Center of Mass" +msgstr "Показати &Центр Масс" + +#: graphicswin.cpp:177 +msgid "Show &Underconstrained Points" +msgstr "Показати &Надмірно Обмежені Точки" + +#: graphicswin.cpp:179 +msgid "&Trace Point" +msgstr "&Трасувати Точку" + +#: graphicswin.cpp:180 +msgid "&Stop Tracing..." +msgstr "&Зупити Трасування..." + +#: graphicswin.cpp:181 +msgid "Step &Dimension..." +msgstr "Прорахувати &Розмір..." + +#: graphicswin.cpp:183 +msgid "&Help" +msgstr "&Довідка" + +#: graphicswin.cpp:184 +msgid "&Language" +msgstr "&Мова" + +#: graphicswin.cpp:185 +msgid "&Website / Manual" +msgstr "&Вебсайт / Посібник" + +#: graphicswin.cpp:186 +msgid "&Go to GitHub commit" +msgstr "&До коміту на GitHub" + +#: graphicswin.cpp:188 +msgid "&About" +msgstr "&Про програму" + +#: graphicswin.cpp:362 +msgid "(no recent files)" +msgstr "(нємає нещодавніх файлів)" + +#: graphicswin.cpp:370 +#, c-format +msgid "File '%s' does not exist." +msgstr "Файл '%s' відсутній." + +#: graphicswin.cpp:779 +msgid "No workplane is active, so the grid will not appear." +msgstr "Відсутня активна площина - сітка не відображатиметься." + +#: graphicswin.cpp:794 +msgid "" +"The perspective factor is set to zero, so the view will always be a parallel " +"projection.\n" +"\n" +"For a perspective projection, modify the perspective factor in the " +"configuration screen. A value around 0.3 is typical." +msgstr "" +"Встановлено нульовий коефіцієнт перспективи, тому відображення завжди буде " +"паралельною проєкцією." + +#: graphicswin.cpp:884 +msgid "" +"Select a point; this point will become the center of the view on screen." +msgstr "Оберіть точку. Ця точка стане центром відображення на екрані." + +#: graphicswin.cpp:1193 +msgid "No additional entities share endpoints with the selected entities." +msgstr "Жодні сутності не мають спільних кінцевих точок з обраними сутностями." + +#: graphicswin.cpp:1211 +msgid "" +"To use this command, select a point or other entity from an linked part, or " +"make a link group the active group." +msgstr "" +"Для використання цієї команди оберіть точку або іншу сутність з приєднаної " +"деталі, або зробіть приєднану групу активною." + +#: graphicswin.cpp:1234 +msgid "" +"No workplane is active. Activate a workplane (with Sketch -> In Workplane) " +"to define the plane for the snap grid." +msgstr "" +"Жодної активної площини. Активуйте площину (в Креслення -> У Робочій " +"Площині) щоб визначити площину для сітки." + +#: graphicswin.cpp:1241 +msgid "" +"Can't snap these items to grid; select points, text comments, or constraints " +"with a label. To snap a line, select its endpoints." +msgstr "" +"Неможливо прикріпити ці штуки до сітки; оберіть точки, текстові коментарі чи " +"обмеження з назвою. Щоб прикріпити лінію, оберіть її кінцеві точки." + +#: graphicswin.cpp:1326 +msgid "No workplane selected. Activating default workplane for this group." +msgstr "" +"Жодної робочої площини не обрано. Активую площину за замовченням для цієї " +"групи." + +#: graphicswin.cpp:1329 +msgid "" +"No workplane is selected, and the active group does not have a default " +"workplane. Try selecting a workplane, or activating a sketch-in-new-" +"workplane group." +msgstr "" +"Жодної робочої площини не обрано, і активна група не має площини за " +"замовченням. Спробуйте обрати площину, або активувати групу Креслення У " +"Робочій Площині." + +#: graphicswin.cpp:1350 +msgid "" +"Bad selection for tangent arc at point. Select a single point, or select " +"nothing to set up arc parameters." +msgstr "" +"Поганий вибір для дотичної дуги у точці. Оберіть одну точку, або оберіть " +"нічого щоб налаштувати дугу." + +#: graphicswin.cpp:1361 +msgid "click point on arc (draws anti-clockwise)" +msgstr "клікніть для встановлення точки дуги (проти годинникової стрілки)" + +#: graphicswin.cpp:1362 +msgid "click to place datum point" +msgstr "клікніть для встановлення вихідної точки" + +#: graphicswin.cpp:1363 +msgid "click first point of line segment" +msgstr "клікніть першу точку прямої лінії" + +#: graphicswin.cpp:1365 +msgid "click first point of construction line segment" +msgstr "клікніть першу точку конструктивної прямої лінії" + +#: graphicswin.cpp:1366 +msgid "click first point of cubic segment" +msgstr "клікніть першу точку кривої" + +#: graphicswin.cpp:1367 +msgid "click center of circle" +msgstr "клікніть в місце де буде центр коментаря" + +#: graphicswin.cpp:1368 +msgid "click origin of workplane" +msgstr "клікніть в центр відліку площини" + +#: graphicswin.cpp:1369 +msgid "click one corner of rectangle" +msgstr "клікніть для встановлення першого кута прямокутника" + +#: graphicswin.cpp:1370 +msgid "click top left of text" +msgstr "клікніть для встановлення верхньої лівої межі тексту" + +#: graphicswin.cpp:1376 +msgid "click top left of image" +msgstr "клікніть для встановлення верхньої лівої межі зображення" + +#: graphicswin.cpp:1402 +msgid "" +"No entities are selected. Select entities before trying to toggle their " +"construction state." +msgstr "Жодної сутності не обрано. Оберіть сутності для перключення їх стану." + +#: group.cpp:86 +msgctxt "group-name" +msgid "sketch-in-3d" +msgstr "ескіз-в-3D" + +#: group.cpp:154 +msgid "" +"Bad selection for new sketch in workplane. This group can be created with:\n" +"\n" +" * a point (through the point, orthogonal to coordinate axes)\n" +" * a point and two line segments (through the point, parallel to the " +"lines)\n" +" * a point and a normal (through the point, orthogonal to the normal)\n" +" * a workplane (copy of the workplane)\n" +msgstr "" +"Поганий вибір для ного креслення в робочій площині. Ця група може бути " +"створена з:\n" +"\n" +" * точки (через точку, ортогонально до координатних осей)\n" +" * точки та двох відрізків (через точку, паралельно до відрізків)\n" +" * точку та перпендикуляр (через точку, ортогонально перпендикуляру)\n" +" * робочої площини (створиться копія площини)\n" + +#: group.cpp:170 +msgid "" +"Activate a workplane (Sketch -> In Workplane) before extruding. The sketch " +"will be extruded normal to the workplane." +msgstr "" +"Активуйте робочу площину ( Ескіз -> У Площині) перед екструдуванням. Ескіз " +"буде екструдовано перпендикулярно до робочої площини." + +#: group.cpp:179 +msgctxt "group-name" +msgid "extrude" +msgstr "видавлювання" + +#: group.cpp:184 +msgid "Lathe operation can only be applied to planar sketches." +msgstr "" +"Операція виточування може бути застосована тільки для пласких креслень." + +#: group.cpp:195 +msgid "" +"Bad selection for new lathe group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Поганий вибір для нової групи проточування. Ця група може бути створена з:\n" +"\n" +" * точки та відрізка або перпендикуляра (обертається крізь точку навколо " +"осі, паралельної до відрізку / перпендикуляру)\n" +" * відрізка (обертається навколо відрізка)\n" + +#: group.cpp:205 +msgctxt "group-name" +msgid "lathe" +msgstr "проточування" + +#: group.cpp:210 +msgid "Revolve operation can only be applied to planar sketches." +msgstr "" +"Операція прокручування може бути застосована тільки для пласких креслень." + +#: group.cpp:221 +msgid "" +"Bad selection for new revolve group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Поганий вибір для нової групи прокручування. Ця група може бути створена з:\n" +"\n" +" * точки та відрізка або перпендикуляра (обертається крізь точку навколо " +"осі, паралельної до відрізку / перпендикуляру)\n" +" * відрізка (обертається навколо відрізка)\n" + +#: group.cpp:233 +msgctxt "group-name" +msgid "revolve" +msgstr "прокручування" + +#: group.cpp:238 +msgid "Helix operation can only be applied to planar sketches." +msgstr "Спіраль може бути створена лише на основі площинного ескізу." + +#: group.cpp:249 +msgid "" +"Bad selection for new helix group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"Поганий вибір для нової групи проточування. Ця група може бути створена з:\n" +"\n" +" * точки та відрізка або перпендикуляра (обертається крізь точку навколо " +"осі, паралельної до відрізку / перпендикуляру)\n" +" * відрізка (обертається навколо відрізка)\n" + +#: group.cpp:261 +msgctxt "group-name" +msgid "helix" +msgstr "спіраль" + +#: group.cpp:274 +msgid "" +"Bad selection for new rotation. This group can be created with:\n" +"\n" +" * a point, while locked in workplane (rotate in plane, about that " +"point)\n" +" * a point and a line or a normal (rotate about an axis through the " +"point, and parallel to line / normal)\n" +msgstr "" +"Поганий вибір для нової групи проточування. Ця група може бути створена з:\n" +"\n" +" * точки, доки закріплено в робочій площині (обертається в площині, " +"навколо цієї точки)\n" +" * точки та відрізка чи перпендикуляря (обертається навколо осі, що " +"проходить крізь цю точку, і паралельна до відрізку / перпендикуляру)\n" + +#: group.cpp:287 +msgctxt "group-name" +msgid "rotate" +msgstr "крутіння" + +#: group.cpp:298 +msgctxt "group-name" +msgid "translate" +msgstr "перекладання" + +#: group.cpp:422 +msgid "(unnamed)" +msgstr "(безіменне)" + +#: groupmesh.cpp:710 +msgid "not closed contour, or not all same style!" +msgstr "не замкнений контур або не все в єдиному стилі!" + +#: groupmesh.cpp:723 +msgid "points not all coplanar!" +msgstr "не всі точки знаходяться в одній площині!" + +#: groupmesh.cpp:725 +msgid "contour is self-intersecting!" +msgstr "контур самоперетинається!" + +#: groupmesh.cpp:727 +msgid "zero-length edge!" +msgstr "ребро нульової довжини!" + +#: importmesh.cpp:136 +msgid "Text-formated STL files are not currently supported" +msgstr "Файли STL з текстовим форматуванням наразі не підтримуються" + +#: modify.cpp:252 +msgid "Must be sketching in workplane to create tangent arc." +msgstr "" +"Для створення дотичної дуги має бути обраний режим креслення в робочій " +"площині." + +#: modify.cpp:299 +msgid "" +"To create a tangent arc, select a point where two non-construction lines or " +"circles in this group and workplane join." +msgstr "" +"Для створення дотичної дуги оберіть точку де два відрізки або дуги " +"поєднуються." + +#: modify.cpp:386 +msgid "" +"Couldn't round this corner. Try a smaller radius, or try creating the " +"desired geometry by hand with tangency constraints." +msgstr "" +"Неможливо закруглити цей кут. Спробуйте менший радіус або створіть необхідну " +"геометрію вручну задопомогою омеження Дотичності." + +#: modify.cpp:595 +msgid "Couldn't split this entity; lines, circles, or cubics only." +msgstr "" +"Неможливо розділити цей об'єкт. Дозволено тільки для відрізків, кіл(дуг), чи " +"сплайнів." + +#: modify.cpp:622 +msgid "Must be sketching in workplane to split." +msgstr "" +"Креслення має відбуватися у робочій площині для використання розділення." + +#: modify.cpp:629 +msgid "" +"Select two entities that intersect each other (e.g. two lines/circles/arcs " +"or a line/circle/arc and a point)." +msgstr "" +"Оберіть дві сутності, що перетинаються (наприклад лінії/кола/дуги або лінія/" +"коло/дуга та точка)" + +#: modify.cpp:734 +msgid "Can't split; no intersection found." +msgstr "Неможливо розділити; відсутній перетин." + +#: mouse.cpp:558 +msgid "Assign to Style" +msgstr "Встановити Стиль" + +#: mouse.cpp:574 +msgid "No Style" +msgstr "Без Стилю" + +#: mouse.cpp:577 +msgid "Newly Created Custom Style..." +msgstr "Створити Користувацький Стиль..." + +#: mouse.cpp:584 +msgid "Group Info" +msgstr "Параметри Групи" + +#: mouse.cpp:604 +msgid "Style Info" +msgstr "Параметри Стилю" + +#: mouse.cpp:624 +msgid "Select Edge Chain" +msgstr "Виділити Ланцюг Ребер" + +#: mouse.cpp:630 +msgid "Toggle Reference Dimension" +msgstr "Перемкнути Відносність Розміру" + +#: mouse.cpp:636 +msgid "Other Supplementary Angle" +msgstr "Інший Суміжний Кут" + +#: mouse.cpp:641 +msgid "Snap to Grid" +msgstr "Прикріпити до Сітки" + +#: mouse.cpp:650 +msgid "Remove Spline Point" +msgstr "Видалити Точку Сплайну" + +#: mouse.cpp:685 +msgid "Add Spline Point" +msgstr "Додати Точку Сплайну" + +#: mouse.cpp:689 +msgid "Cannot add spline point: maximum number of points reached." +msgstr "" +"Неможливо додати точку сплайна: перевищено максимальну кількість точок." + +#: mouse.cpp:714 +msgid "Toggle Construction" +msgstr "Пермкнути Конструктивність" + +#: mouse.cpp:730 +msgid "Delete Point-Coincident Constraint" +msgstr "Роз'єднати З'єднання Точок" + +#: mouse.cpp:748 +msgid "Cut" +msgstr "Вирізати" + +#: mouse.cpp:750 +msgid "Copy" +msgstr "Копіювати" + +#: mouse.cpp:754 +msgid "Select All" +msgstr "Виділити Усе" + +#: mouse.cpp:759 +msgid "Paste" +msgstr "Вставити" + +#: mouse.cpp:761 +msgid "Paste Transformed..." +msgstr "Вставити Трансформованим..." + +#: mouse.cpp:766 +msgid "Delete" +msgstr "Видалити" + +#: mouse.cpp:769 +msgid "Unselect All" +msgstr "Зняти Виділення з Усього" + +#: mouse.cpp:776 +msgid "Unselect Hovered" +msgstr "Зняти Виділення з Наведеного" + +#: mouse.cpp:785 +msgid "Zoom to Fit" +msgstr "Умістити на Екрані" + +#: mouse.cpp:987 mouse.cpp:1276 +msgid "click next point of line, or press Esc" +msgstr "клікніть наступну точку лінії або натисніть Esc" + +#: mouse.cpp:993 +msgid "" +"Can't draw rectangle in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Неможливо накреслити прямокутник у 3d; спочатку активуйте робочу площину в " +"Креслення -> У Робочій Площині." + +#: mouse.cpp:1027 +msgid "click to place other corner of rectangle" +msgstr "клікніть для встановлення іншого кута прямокутника" + +#: mouse.cpp:1048 +msgid "click to set radius" +msgstr "клікніть для визначення радіусу" + +#: mouse.cpp:1053 +msgid "" +"Can't draw arc in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Неможливо накреслити дугу у 3d; спочатку активуйте робочу площину в " +"Креслення -> У Робочій Площині." + +#: mouse.cpp:1072 +msgid "click to place point" +msgstr "клікніть для встановлення точки" + +#: mouse.cpp:1088 +msgid "click next point of cubic, or press Esc" +msgstr "клікніть наступну точку кривої або натисніть Esc" + +#: mouse.cpp:1093 +msgid "" +"Sketching in a workplane already; sketch in 3d before creating new workplane." +msgstr "" +"Вже обрано креслення у робочій площині; поверніться у креслення будь-ду в 3D " +"для створення нової робочої площини." + +#: mouse.cpp:1109 +msgid "" +"Can't draw text in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Неможливо накреслити текст у 3д; спочатку активуйте робочу площину в " +"Креслення -> У Робочій Площині." + +#: mouse.cpp:1126 +msgid "click to place bottom right of text" +msgstr "клікніть для встановлення нижньої правої межі тексту" + +#: mouse.cpp:1132 +msgid "" +"Can't draw image in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "" +"Неможливо накреслити зображення у 3д; спочатку активуйте робочу площину в " +"Креслення -> У Робочій Площині." + +#: platform/gui.cpp:85 platform/gui.cpp:90 solvespace.cpp:583 +msgctxt "file-type" +msgid "SolveSpace models" +msgstr "SolveSpace модел" + +#: platform/gui.cpp:89 +msgctxt "file-type" +msgid "ALL" +msgstr "УСІ" + +#: platform/gui.cpp:91 +msgctxt "file-type" +msgid "IDF circuit board" +msgstr "IDF друкована плата" + +#: platform/gui.cpp:92 +msgctxt "file-type" +msgid "STL triangle mesh" +msgstr "STL трикутникова сітка" + +#: platform/gui.cpp:96 +msgctxt "file-type" +msgid "PNG image" +msgstr "PNG зображення" + +#: platform/gui.cpp:100 +msgctxt "file-type" +msgid "STL mesh" +msgstr "STL сітка" + +#: platform/gui.cpp:101 +msgctxt "file-type" +msgid "Wavefront OBJ mesh" +msgstr "Wavefront OBJ меш" + +#: platform/gui.cpp:102 +msgctxt "file-type" +msgid "Three.js-compatible mesh, with viewer" +msgstr "Three.js-сумісний меш, з переглядачем" + +#: platform/gui.cpp:103 +msgctxt "file-type" +msgid "Three.js-compatible mesh, mesh only" +msgstr "Three.js-сумісний меш, лише меш" + +#: platform/gui.cpp:104 +msgctxt "file-type" +msgid "VRML text file" +msgstr "VRML меш, текстовий формат" + +#: platform/gui.cpp:108 platform/gui.cpp:115 platform/gui.cpp:122 +msgctxt "file-type" +msgid "STEP file" +msgstr "STEP файл" + +#: platform/gui.cpp:112 +msgctxt "file-type" +msgid "PDF file" +msgstr "PDF файл" + +#: platform/gui.cpp:113 +msgctxt "file-type" +msgid "Encapsulated PostScript" +msgstr "Encapsulated PostScript" + +#: platform/gui.cpp:114 +msgctxt "file-type" +msgid "Scalable Vector Graphics" +msgstr "Scalable Vector Graphics, векторний формат" + +#: platform/gui.cpp:116 platform/gui.cpp:123 +msgctxt "file-type" +msgid "DXF file (AutoCAD 2007)" +msgstr "DXF файл (AutoCAD 2007)" + +#: platform/gui.cpp:117 +msgctxt "file-type" +msgid "HPGL file" +msgstr "HPGL файл" + +#: platform/gui.cpp:118 +msgctxt "file-type" +msgid "G Code" +msgstr "G Code" + +#: platform/gui.cpp:127 +msgctxt "file-type" +msgid "AutoCAD DXF and DWG files" +msgstr "AutoCAD DXF та DWG файли" + +#: platform/gui.cpp:131 +msgctxt "file-type" +msgid "Comma-separated values" +msgstr "Comma-separated values" + +#: platform/guigtk.cpp:1434 platform/guimac.mm:1513 platform/guiwin.cpp:1641 +msgid "untitled" +msgstr "без імені" + +#: platform/guigtk.cpp:1445 platform/guigtk.cpp:1481 platform/guimac.mm:1471 +#: platform/guiwin.cpp:1639 +msgctxt "title" +msgid "Save File" +msgstr "Зберегти Файл" + +#: platform/guigtk.cpp:1446 platform/guigtk.cpp:1482 platform/guimac.mm:1454 +#: platform/guiwin.cpp:1645 +msgctxt "title" +msgid "Open File" +msgstr "Відкрити Файл" + +#: platform/guigtk.cpp:1449 platform/guigtk.cpp:1488 +msgctxt "button" +msgid "_Cancel" +msgstr "_Скасувати" + +#: platform/guigtk.cpp:1450 platform/guigtk.cpp:1486 +msgctxt "button" +msgid "_Save" +msgstr "_Зберегти" + +#: platform/guigtk.cpp:1451 platform/guigtk.cpp:1487 +msgctxt "button" +msgid "_Open" +msgstr "_Відкрити" + +#: solvespace.cpp:175 +msgctxt "title" +msgid "Autosave Available" +msgstr "Наявні автозбереження" + +#: solvespace.cpp:176 +msgctxt "dialog" +msgid "An autosave file is available for this sketch." +msgstr "Наявні автозбереження для цього креслення." + +#: solvespace.cpp:177 +msgctxt "dialog" +msgid "Do you want to load the autosave file instead?" +msgstr "Завантажити файл автозбереження?" + +#: solvespace.cpp:178 +msgctxt "button" +msgid "&Load autosave" +msgstr "&Завантажити автозбереження" + +#: solvespace.cpp:180 +msgctxt "button" +msgid "Do&n't Load" +msgstr "&Не Завантажувати" + +#: solvespace.cpp:640 +msgctxt "title" +msgid "Modified File" +msgstr "Файл Змінено" + +#: solvespace.cpp:642 +#, c-format +msgctxt "dialog" +msgid "Do you want to save the changes you made to the sketch “%s”?" +msgstr "Чи хочете ви зберегти зміни зроблені вами у ескізі “%s”?" + +#: solvespace.cpp:645 +msgctxt "dialog" +msgid "Do you want to save the changes you made to the new sketch?" +msgstr "Чи хочете ви зберегти зміни зроблені вами у новому ескізі?" + +#: solvespace.cpp:648 +msgctxt "dialog" +msgid "Your changes will be lost if you don't save them." +msgstr "Ваші зміни буде втрачено якщо ви не збережете їх." + +#: solvespace.cpp:649 +msgctxt "button" +msgid "&Save" +msgstr "&Зберегти" + +#: solvespace.cpp:651 +msgctxt "button" +msgid "Do&n't Save" +msgstr "&Не Зберігати" + +#: solvespace.cpp:672 +msgctxt "title" +msgid "(new sketch)" +msgstr "(нове креслення)" + +#: solvespace.cpp:683 +msgctxt "title" +msgid "Property Browser" +msgstr "Браузер Властивостей" + +#: solvespace.cpp:746 +msgid "" +"Constraints are currently shown, and will be exported in the toolpath. This " +"is probably not what you want; hide them by clicking the link at the top of " +"the text window." +msgstr "" +"Зараз обмеження відображаються, і будуть експортовані в шлях інструменту. " +"Скоріш за все, це не те, чого ви домагаєтеся; cховайте їх клікнувши на " +"посилання над текстовим вікном." + +#: solvespace.cpp:834 +#, c-format +msgid "" +"Can't identify file type from file extension of filename '%s'; try .dxf " +"or .dwg." +msgstr "" +"Неможливо визначити тип файлу з розширення '%s'; спробуйте .dxf чи .dwg." + +#: solvespace.cpp:886 +msgid "Constraint must have a label, and must not be a reference dimension." +msgstr "Обмежувач має містити мітку і бути не відносним розміром." + +#: solvespace.cpp:890 +msgid "Bad selection for step dimension; select a constraint." +msgstr "Поганий вибір для крокової зміни розміру; оберіть обмежувач." + +#: solvespace.cpp:914 +msgid "The assembly does not interfere, good." +msgstr "Збірка не перетинається, очман." + +#: solvespace.cpp:930 +#, c-format +msgid "" +"The volume of the solid model is:\n" +"\n" +" %s" +msgstr "" +"Об'єм твердого тіла становить:\n" +"\n" +" %s" + +#: solvespace.cpp:939 +#, c-format +msgid "" +"\n" +"The volume of current group mesh is:\n" +"\n" +" %s" +msgstr "" +"\n" +"Об'єм поточної групи мешу становить:\n" +"\n" +" %s" + +#: solvespace.cpp:944 +msgid "" +"\n" +"\n" +"Curved surfaces have been approximated as triangles.\n" +"This introduces error, typically of around 1%." +msgstr "" +"\n" +"\n" +"Скруглені поверхні були апроксимовані до трикутників.\n" +"Це створює похибку, зазвичай біля 1%." + +#: solvespace.cpp:959 +#, c-format +msgid "" +"The surface area of the selected faces is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"Площа поверхні обраної грані:\n" +"\n" +" %s\n" +"\n" +"Криві були апроксимовані як дрібні відрізки.\n" +"Це створює похибку, зазвичай біля 1%%." + +#: solvespace.cpp:968 +msgid "" +"This group does not contain a correctly-formed 2d closed area. It is open, " +"not coplanar, or self-intersecting." +msgstr "" +"Ця група не місить коректно сформованого замкненої 2D площини. Вона " +"відкрита, не компланарна, або ж самоперетинається." + +#: solvespace.cpp:980 +#, c-format +msgid "" +"The area of the region sketched in this group is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"Площа заповнення ескізу у цій групі становить:\n" +"\n" +" %s\n" +"\n" +"Криві наближено до ламаних ліній.\n" +"Це вносить похибку, зазвичай близько 1%%." + +#: solvespace.cpp:1000 +#, c-format +msgid "" +"The total length of the selected entities is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"Загальна довжина обраних елементів становить:\n" +"\n" +" %s\n" +"\n" +"Криві наближено до ламаних ліній.\n" +"Це вносить похибку, зазвичай близько 1%%." + +#: solvespace.cpp:1006 +msgid "Bad selection for perimeter; select line segments, arcs, and curves." +msgstr "Поганий вибір для периметру; оберіть відрізки, дуги та криві." + +#: solvespace.cpp:1022 +msgid "Bad selection for trace; select a single point." +msgstr "Поганий вибір для вістежування шляху; оберіть одну точку." + +#: solvespace.cpp:1049 +#, c-format +msgid "Couldn't write to '%s'" +msgstr "Неможливо записати у '%s'" + +#: solvespace.cpp:1079 +msgid "The mesh is self-intersecting (NOT okay, invalid)." +msgstr "Меш самоперетинається (НЕ добре, недійсний)." + +#: solvespace.cpp:1080 +msgid "The mesh is not self-intersecting (okay, valid)." +msgstr "Меш самоперетинається (добре, дійсний)." + +#: solvespace.cpp:1082 +msgid "The mesh has naked edges (NOT okay, invalid)." +msgstr "Меш містить оголені ребра (НЕ добре, недійсний)." + +#: solvespace.cpp:1083 +msgid "The mesh is watertight (okay, valid)." +msgstr "Меш водонепроникний (добре, дійсний)." + +#: solvespace.cpp:1086 +#, c-format +msgid "" +"\n" +"\n" +"The model contains %d triangles, from %d surfaces." +msgstr "" +"\n" +"\n" +"Модель складається з %d трикутників, що розташовані на %d поверхнях." + +#: solvespace.cpp:1090 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"Zero problematic edges, good.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"Відсутні проблемні ребра, добре.%s" + +#: solvespace.cpp:1093 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d problematic edges, bad.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d проблемних ребер, погано.%s" + +#: solvespace.cpp:1106 +#, c-format +msgid "" +"This is SolveSpace version %s.\n" +"\n" +"For more information, see http://solvespace.com/\n" +"\n" +"SolveSpace is free software: you are free to modify\n" +"and/or redistribute it under the terms of the GNU\n" +"General Public License (GPL) version 3 or later.\n" +"\n" +"There is NO WARRANTY, to the extent permitted by\n" +"law. For details, visit http://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues and other authors.\n" +msgstr "" +"Це SolveSpace версії %s.\n" +"\n" +"Більше інформації на сайті http://solvespace.com/\n" +"\n" +"SolveSpace є вільною програмою: ви можете модифікувати\n" +"та/або розповсюджувати її відповідно до ліцензії GNU\n" +"General Public License (GPL) версії 3 чи новішої.\n" +"\n" +"ВІЧСУТНІ БУДЬ-ЯКІ ГАРАНТІЇ, в межах, доволених\n" +"законом. Деталі на сайті http://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues та інші автори.\n" + +#: style.cpp:185 +msgid "" +"Can't assign style to an entity that's derived from another entity; try " +"assigning a style to this entity's parent." +msgstr "" +"Неможливо призначити стиль елементу який походить від іншого елемента; " +"спробуйте призначити стиль батьківському елементу." + +#: style.cpp:735 +msgid "Style name cannot be empty" +msgstr "Стиль не може містити порожнє ім'я" + +#: textscreens.cpp:837 +msgid "Can't repeat fewer than 1 time." +msgstr "Не можливо повторити менше 1 разу." + +#: textscreens.cpp:841 +msgid "Can't repeat more than 999 times." +msgstr "Не можливо повторити понад 999 разів." + +#: textscreens.cpp:866 +msgid "Group name cannot be empty" +msgstr "Група не може містити порожнє ім'я" + +#: textscreens.cpp:918 +msgid "Opacity must be between zero and one." +msgstr "Непрозорість має бути між 0 та 1." + +#: textscreens.cpp:953 +msgid "Radius cannot be zero or negative." +msgstr "Радіус не може бути нульовим чи від'ємним." + +#: toolbar.cpp:18 +msgid "Sketch line segment" +msgstr "Накреслети відрізок прямої" + +#: toolbar.cpp:20 +msgid "Sketch rectangle" +msgstr "Накреслити прямокутник" + +#: toolbar.cpp:22 +msgid "Sketch circle" +msgstr "Накреслити коло" + +#: toolbar.cpp:24 +msgid "Sketch arc of a circle" +msgstr "Накреслити дугу кола" + +#: toolbar.cpp:26 +msgid "Sketch curves from text in a TrueType font" +msgstr "Накреслити криві з тексту на TrueType шрифті" + +#: toolbar.cpp:28 +msgid "Sketch image from a file" +msgstr "Вставити зображення з файлу" + +#: toolbar.cpp:30 +msgid "Create tangent arc at selected point" +msgstr "Створити дотичну дугу у вибраній точці" + +#: toolbar.cpp:32 +msgid "Sketch cubic Bezier spline" +msgstr "Накреслити кубічний сплайн Без'є" + +#: toolbar.cpp:34 +msgid "Sketch datum point" +msgstr "Накреслити опорну точку" + +#: toolbar.cpp:36 +msgid "Toggle construction" +msgstr "Перемкнути конструктивність" + +#: toolbar.cpp:38 +msgid "Split lines / curves where they intersect" +msgstr "Розрізати лініх / криві в місцях перетину" + +#: toolbar.cpp:42 +msgid "Constrain distance / diameter / length" +msgstr "Визначити відстань / діаметр / довжину" + +#: toolbar.cpp:44 +msgid "Constrain angle" +msgstr "Визначити кут" + +#: toolbar.cpp:46 +msgid "Constrain to be horizontal" +msgstr "Встановити горизонтально" + +#: toolbar.cpp:48 +msgid "Constrain to be vertical" +msgstr "Встановити перпендикулярно" + +#: toolbar.cpp:50 +msgid "Constrain to be parallel or tangent" +msgstr "Встановити паралельно або дотично" + +#: toolbar.cpp:52 +msgid "Constrain to be perpendicular" +msgstr "Встановити перпендикулярно" + +#: toolbar.cpp:54 +msgid "Constrain point on line / curve / plane / point" +msgstr "Прив'язати точку до лінії / кривої / площини / точки" + +#: toolbar.cpp:56 +msgid "Constrain symmetric" +msgstr "Встановити симетрично" + +#: toolbar.cpp:58 +msgid "Constrain equal length / radius / angle" +msgstr "Встановити однаковими відстань / радіус / кут" + +#: toolbar.cpp:60 +msgid "Constrain normals in same orientation" +msgstr "Спрямувати нормалі в одному напрямку" + +#: toolbar.cpp:62 +msgid "Other supplementary angle" +msgstr "Інший суміжний кут" + +#: toolbar.cpp:64 +msgid "Toggle reference dimension" +msgstr "Перемкнути відносність розміру" + +#: toolbar.cpp:68 +msgid "New group extruding active sketch" +msgstr "Нова група екструдування активного ескізу" + +#: toolbar.cpp:70 +msgid "New group rotating active sketch" +msgstr "Нова група обертання актиного ескізу" + +#: toolbar.cpp:72 +msgid "New group helix from active sketch" +msgstr "Нова група спіралі з активного ескізу" + +#: toolbar.cpp:74 +msgid "New group revolve active sketch" +msgstr "Нова група обертання активного ескізу" + +#: toolbar.cpp:76 +msgid "New group step and repeat rotating" +msgstr "Нова група крокування і повторення обертання" + +#: toolbar.cpp:78 +msgid "New group step and repeat translating" +msgstr "Нова група крокування і повторення зміщення" + +#: toolbar.cpp:80 +msgid "New group in new workplane (thru given entities)" +msgstr "Нова група в новій площині (через обрані об'екти)" + +#: toolbar.cpp:82 +msgid "New group in 3d" +msgstr "Нова група в 3D" + +#: toolbar.cpp:84 +msgid "New group linking / assembling file" +msgstr "Нова група приєднання / монтування файлу" + +#: toolbar.cpp:88 +msgid "Nearest isometric view" +msgstr "Найближчий ізометричний вигляд" + +#: toolbar.cpp:90 +msgid "Align view to active workplane" +msgstr "Вирівняти вигляд до активної робочої площини" + +#: util.cpp:165 +msgctxt "title" +msgid "Error" +msgstr "Помилка" + +#: util.cpp:165 +msgctxt "title" +msgid "Message" +msgstr "Повідомлення" + +#: util.cpp:170 +msgctxt "button" +msgid "&OK" +msgstr "&OK" + +#: view.cpp:127 +msgid "Scale cannot be zero or negative." +msgstr "Масштаб не може бути нульовим чи від'ємним." + +#: view.cpp:139 view.cpp:148 +msgid "Bad format: specify x, y, z" +msgstr "Некоректний формат: визначте X, Y, Z" + +#~ msgid "A&ngle" +#~ msgstr "К&ут" + +#~ msgid "E&qual Length / Radius / Angle" +#~ msgstr "Рі&вні Довжина / Радіус / Кут" + +#~ msgid "Length Ra&tio" +#~ msgstr "Про&порція Довжин" + +#~ msgid "Length Diff&erence" +#~ msgstr "Рі&зниця Довжин" + +#~ msgid "Show Degrees of &Freedom" +#~ msgstr "Показати Степені &Свободи" + +#~ msgid "Show Menu &Bar" +#~ msgstr "Показати Панель &Меню" diff --git a/res/locales/zh_CN.po b/res/locales/zh_CN.po new file mode 100644 index 0000000..2a992b4 --- /dev/null +++ b/res/locales/zh_CN.po @@ -0,0 +1,2383 @@ +# Chinese translations for SolveSpace package. +# Copyright (C) 2020 the PACKAGE authors +# This file is distributed under the same license as the SolveSpace package. +# , 2020. +# , 2023. +# , 2025. +# +# 译词一致性 +# mesh网格 grid格线 +# text文本 comment备注 +# vertical竖直 perpendicular垂直 +# Rotate旋转 Lathe转圈 Revolve扫略 +# solid实心体 object对象 entity物件 +# plane点线面/平面 face/surface表面 plane_faces平表面 +# +# 约束的选择错误。此约束可用于:A与(B和C) +# 一点/工作面 一条线段 一个表面 一个以上 +# +msgid "" +msgstr "" +"Project-Id-Version: SolveSpace 3.2\n" +"Report-Msgid-Bugs-To: phkahler@gmail.com\n" +"POT-Creation-Date: 2025-01-26 21:04+0200\n" +"PO-Revision-Date: 2023-04-30 15:58+0800\n" +"Last-Translator: liuxilu@live.com\n" +"Language-Team: none\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.4.2\n" + +#: clipboard.cpp:314 +msgid "" +"Cut, paste, and copy work only in a workplane.\n" +"\n" +"Activate one with Sketch -> In Workplane." +msgstr "" +"剪切、粘贴、复制在工作面内才可用。\n" +"\n" +"用\"绘图 -> 在工作面内\"来激活一个。" + +#: clipboard.cpp:331 +msgid "Clipboard is empty; nothing to paste." +msgstr "剪贴板为空;没有能粘贴的。" + +#: clipboard.cpp:378 +msgid "Number of copies to paste must be at least one." +msgstr "至少要粘贴 1 个副本。" + +#: clipboard.cpp:394 textscreens.cpp:879 +msgid "Scale cannot be zero." +msgstr "缩放不能为零。" + +#: clipboard.cpp:436 +msgid "Select one point to define origin of rotation." +msgstr "选择一个点来定义旋转中心。" + +#: clipboard.cpp:448 +msgid "Select two points to define translation vector." +msgstr "选择两个点来定义平移向量。" + +#: clipboard.cpp:458 +msgid "" +"Transformation is identity. So all copies will be exactly on top of each " +"other." +msgstr "输入的变换是不动,因此所有副本将互相重叠。" + +#: clipboard.cpp:462 +msgid "Too many items to paste; split this into smaller pastes." +msgstr "要粘贴的项目太多; 请把他们拆分。" + +#: clipboard.cpp:467 +msgid "No workplane active." +msgstr "无活动工作面。" + +#: confscreen.cpp:410 +msgid "Bad format: specify coordinates as x, y, z" +msgstr "格式错误:请用 x,y,z 指定坐标" + +#: confscreen.cpp:420 style.cpp:729 textscreens.cpp:910 +msgid "Bad format: specify color as r, g, b" +msgstr "格式错误:请用 r,g,b 指定颜色" + +#: confscreen.cpp:446 +msgid "" +"The perspective factor will have no effect until you enable View -> Use " +"Perspective Projection." +msgstr "若不启用\"查看 -> 使用透视投影\",透视因数将不起作用。" + +#: confscreen.cpp:464 confscreen.cpp:474 +#, c-format +msgid "Specify between 0 and %d digits after the decimal." +msgstr "请指定小数点后 0 到 %d 位。" + +#: confscreen.cpp:486 +msgid "Export scale must not be zero!" +msgstr "导出比例不能为零!" + +#: confscreen.cpp:498 +msgid "Cutter radius offset must not be negative!" +msgstr "刀具半径偏移不能为负!" + +#: confscreen.cpp:557 +msgid "Bad value: autosave interval should be positive" +msgstr "值错误:自动保存间隔应为正" + +#: confscreen.cpp:560 +msgid "Bad format: specify interval in integral minutes" +msgstr "格式错误:间隔应为整分钟" + +#: constraint.cpp:12 +msgctxt "constr-name" +msgid "pts-coincident" +msgstr "点点重合" + +#: constraint.cpp:13 +msgctxt "constr-name" +msgid "pt-pt-distance" +msgstr "点点距离" + +#: constraint.cpp:14 +msgctxt "constr-name" +msgid "pt-line-distance" +msgstr "点线距离" + +#: constraint.cpp:15 +msgctxt "constr-name" +msgid "pt-plane-distance" +msgstr "点面距离" + +#: constraint.cpp:16 +msgctxt "constr-name" +msgid "pt-face-distance" +msgstr "点面距离" + +#: constraint.cpp:17 +msgctxt "constr-name" +msgid "proj-pt-pt-distance" +msgstr "点点投影距离" + +#: constraint.cpp:18 +msgctxt "constr-name" +msgid "pt-in-plane" +msgstr "点在面上" + +#: constraint.cpp:19 +msgctxt "constr-name" +msgid "pt-on-line" +msgstr "点在直线上" + +#: constraint.cpp:20 +msgctxt "constr-name" +msgid "pt-on-face" +msgstr "点在表面上" + +#: constraint.cpp:21 +msgctxt "constr-name" +msgid "eq-length" +msgstr "线长相等" + +#: constraint.cpp:22 +msgctxt "constr-name" +msgid "eq-length-and-pt-ln-dist" +msgstr "线长与点线距离相等" + +#: constraint.cpp:23 +msgctxt "constr-name" +msgid "eq-pt-line-distances" +msgstr "点线距离相等" + +#: constraint.cpp:24 +msgctxt "constr-name" +msgid "length-ratio" +msgstr "线长比例" + +#: constraint.cpp:25 +msgctxt "constr-name" +msgid "arc-arc-length-ratio" +msgstr "弧长比例" + +#: constraint.cpp:26 +msgctxt "constr-name" +msgid "arc-line-length-ratio" +msgstr "弧长与线长比例" + +#: constraint.cpp:27 +msgctxt "constr-name" +msgid "length-difference" +msgstr "线长之差" + +#: constraint.cpp:28 +msgctxt "constr-name" +msgid "arc-arc-len-difference" +msgstr "弧长之差" + +#: constraint.cpp:29 +msgctxt "constr-name" +msgid "arc-line-len-difference" +msgstr "弧长与线长之差" + +#: constraint.cpp:30 +msgctxt "constr-name" +msgid "symmetric" +msgstr "对称" + +#: constraint.cpp:31 +msgctxt "constr-name" +msgid "symmetric-h" +msgstr "水平对称" + +#: constraint.cpp:32 +msgctxt "constr-name" +msgid "symmetric-v" +msgstr "竖直对称" + +#: constraint.cpp:33 +msgctxt "constr-name" +msgid "symmetric-line" +msgstr "关于直线对称" + +#: constraint.cpp:34 +msgctxt "constr-name" +msgid "at-midpoint" +msgstr "中点" + +#: constraint.cpp:35 +msgctxt "constr-name" +msgid "horizontal" +msgstr "æ°´å¹³" + +#: constraint.cpp:36 +msgctxt "constr-name" +msgid "vertical" +msgstr "竖直" + +#: constraint.cpp:37 +msgctxt "constr-name" +msgid "diameter" +msgstr "直径" + +#: constraint.cpp:38 +msgctxt "constr-name" +msgid "pt-on-circle" +msgstr "点在圆上" + +#: constraint.cpp:39 +msgctxt "constr-name" +msgid "same-orientation" +msgstr "同向" + +#: constraint.cpp:40 +msgctxt "constr-name" +msgid "angle" +msgstr "角度" + +#: constraint.cpp:41 +msgctxt "constr-name" +msgid "parallel" +msgstr "平行" + +#: constraint.cpp:42 +msgctxt "constr-name" +msgid "arc-line-tangent" +msgstr "弧和线相切" + +#: constraint.cpp:43 +msgctxt "constr-name" +msgid "cubic-line-tangent" +msgstr "三次曲线和直线相切" + +#: constraint.cpp:44 +msgctxt "constr-name" +msgid "curve-curve-tangent" +msgstr "曲线相切" + +#: constraint.cpp:45 +msgctxt "constr-name" +msgid "perpendicular" +msgstr "垂直" + +#: constraint.cpp:46 +msgctxt "constr-name" +msgid "eq-radius" +msgstr "半径相等" + +#: constraint.cpp:47 +msgctxt "constr-name" +msgid "eq-angle" +msgstr "角度相等" + +#: constraint.cpp:48 +msgctxt "constr-name" +msgid "eq-line-len-arc-len" +msgstr "线长和弧长相等" + +#: constraint.cpp:49 +msgctxt "constr-name" +msgid "lock-where-dragged" +msgstr "定位后锁定" + +#: constraint.cpp:50 +msgctxt "constr-name" +msgid "comment" +msgstr "备注" + +#: constraint.cpp:151 +msgid "" +"The point you selected does not belong to the arc. The arc and line segment " +"do not share an end point.\n" +"\n" +"Select the end point of the arc at which you want it to be tangent to the " +"line." +msgstr "" +"您选择的点不属于弧线, 弧线与线段不共享端点\n" +"\n" +"在样条曲线上选择一个您想要与目标线相切的端点." + +#: constraint.cpp:158 +msgid "" +"The tangent arc and line segment must share an endpoint. Constrain them with " +"Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the arc at which you want it to be " +"tangent to the line." +msgstr "" +"要相切的弧和线段必须有共同端点。约束相切之前,用\"约束 -> 在点上\"来约束它" +"们。\n" +"\n" +"此外, 可以在曲线上选择一个您想要与目标线相切的端点." + +#: constraint.cpp:186 +msgid "" +"The point you selected is not an end point of the cubic spline. The spline " +"and line segment do not share an end point.\n" +"\n" +"Select the end point of the spline at which you want it to be tangent to the " +"line." +msgstr "" +"您选中的点不是三次样条曲线端点. 样条线和线段并不共享端点.\n" +"\n" +"在样条曲线上选择一个您想要与目标线相切的端点." + +#: constraint.cpp:193 +msgid "" +"The tangent cubic spline and line segment must share an endpoint. Constrain " +"them with Constrain -> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the cubic spline at which you want it " +"to be tangent to the line." +msgstr "" +"要相切的三次曲线段和线段必须有共同端点。约束相切之前,使用\"约束 -> 在点上" +"\"来约束它们。\n" +"\n" +"此外, 可以在三次样条曲线上选择一个您想要与目标线相切的端点." + +#: constraint.cpp:237 +msgid "" +"The points you selected are not end points of the two curves. The curves do " +"not share an end point.\n" +"\n" +"Select the end points of both curves at which you want them to be tangent to " +"each other." +msgstr "" +"您选择的端点不属于二个曲线, 曲线并不共有一个端点.\n" +"\n" +"在二个曲线上选择一个您想要二者相切的共有端点." + +#: constraint.cpp:244 +msgid "" +"The curves must share an endpoint. Constrain them with Constrain -> On Point " +"before constraining tangent.\n" +"\n" +"Alternatively select the end points of both curves at which you want the " +"curves to be tangent." +msgstr "" +"要约束的曲线必须有共同端点。约束相切之前,使用\"约束 -> 在点上\"来约束它们。\n" +"\n" +"此外, 可以在二个曲线上选择相切的端点." + +#: constraint.cpp:303 +msgid "" +"Bad selection for distance / diameter constraint. This constraint can apply " +"to:\n" +"\n" +" * two points (distance between points)\n" +" * a line segment (length)\n" +" * two points and a line segment or normal (projected distance)\n" +" * a workplane and a point (minimum distance)\n" +" * a line segment and a point (minimum distance)\n" +" * a plane face and a point (minimum distance)\n" +" * a circle or an arc (diameter)\n" +msgstr "" +"距离/直径约束的选择错误。此约束可用于:\n" +"\n" +" * 两点(点距)\n" +" * 线段(长度)\n" +" * 两点与一条线段或法线(投影距离)\n" +" * 一点与一工作面(最小距离)\n" +" * 一点与一条线段(最小距离)\n" +" * 一点与一平表面(最小距离)\n" +" * 一个圆或一条弧(直径)\n" + +#: constraint.cpp:366 +msgid "" +"Bad selection for on point / curve / plane constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points (points coincident)\n" +" * a point and a workplane (point in plane)\n" +" * a point and a line segment (point on line)\n" +" * a point and a circle or arc (point on curve)\n" +" * a point and one to three plane faces (point on face(s))\n" +msgstr "" +"在点/线/面上约束的选择错误。此约束可用于:\n" +"\n" +" * 两个以上的点(点点重合)\n" +" * 一点与一平面(点在面上)\n" +" * 一点与一条线段(点在直线上)\n" +" * 一点与一个圆或弧(点在曲线上)\n" +" * 一点与一到三个平表面(点在表面上)\n" + +#: constraint.cpp:427 +msgid "" +"Bad selection for equal length / radius constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more line segments (equal length)\n" +" * two line segments and two points (equal point-line distances)\n" +" * a line segment and two points (equal point-line distances)\n" +" * a line segment, and a point and line segment (point-line distance " +"equals length)\n" +" * two or more circles or arcs (equal radius)\n" +" * a line segment and an arc (line segment length equals arc length)\n" +msgstr "" +"长度/半径相等约束的选择错误。此约束可用于:\n" +"\n" +" * 两条以上线段(长度相等)\n" +" * 两条线段与两点(点线距离相等)\n" +" * 一条线段与两点(点线距离相等)\n" +" * 一条线段与一点和线段(点线距离等于长度)\n" +" * 一条线段和一条弧(线长等于弧长)\n" +" * 两个以上圆或弧(半径相等)\n" + +#: constraint.cpp:480 +msgid "" +"Bad selection for length ratio constraint. This constraint can apply to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"长度比例约束的选择错误。此约束可用于:\n" +"\n" +" * 两条线段\n" +" * 两条弧线\n" +" * 一条弧线与一条线段\n" + +#: constraint.cpp:515 +msgid "" +"Bad selection for length difference constraint. This constraint can apply " +"to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" +"长度差约束选择错误。此约束可用于:\n" +"\n" +" * 两条线段\n" +" * 两条弧线\n" +" * 一条弧线与一条线段\n" + +#: constraint.cpp:550 +msgid "" +"Bad selection for at midpoint constraint. This constraint can apply to:\n" +"\n" +" * a line segment and a point (point at midpoint)\n" +" * a line segment and a workplane (line's midpoint on plane)\n" +msgstr "" +"中点约束的选择错误。此约束可用于:\n" +"\n" +" * 一条线段和一点(点在线段中点上)\n" +" * 一条线段和一工作面(线段中点在平面上)\n" + +#: constraint.cpp:608 +msgid "" +"Bad selection for symmetric constraint. This constraint can apply to:\n" +"\n" +" * two points or a line segment (symmetric about workplane's coordinate " +"axis)\n" +" * line segment, and two points or a line segment (symmetric about line " +"segment)\n" +" * workplane, and two points or a line segment (symmetric about " +"workplane)\n" +msgstr "" +"对称约束的选择错误。此约束可用于:\n" +"\n" +" * 两点或一条线段(关于工作面坐标轴对称)\n" +" * 一条线段与两点或一条线段(关于直线对称)\n" +" * 一工作面和两点或一条线段(关于工作面对称)\n" + +#: constraint.cpp:623 +msgid "" +"A workplane must be active when constraining symmetric without an explicit " +"symmetry plane." +msgstr "没有明显的对称面时,约束对称必须有活动工作面。" + +#: constraint.cpp:663 +msgid "" +"Activate a workplane (with Sketch -> In Workplane) before applying a " +"horizontal or vertical constraint." +msgstr "使用水平或竖直约束之前应激活工作面(用\"绘图 -> 在工作面内\")。" + +#: constraint.cpp:679 +msgid "" +"Bad selection for horizontal / vertical constraint. This constraint can " +"apply to:\n" +"\n" +" * two or more points\n" +" * one or more line segments\n" +msgstr "" +"æ°´å¹³/竖直约束的选择错误。此约束可用于:\n" +"\n" +" * 两个以上的点\n" +" * 一条以上的线段\n" + +#: constraint.cpp:697 +msgid "" +"Bad selection for same orientation constraint. This constraint can apply " +"to:\n" +"\n" +" * two normals\n" +msgstr "" +"同向约束的选择错误。此约束可用于:\n" +"\n" +" * 两条法线\n" + +#: constraint.cpp:748 +msgid "Must select an angle constraint." +msgstr "必须选择角度约束。" + +#: constraint.cpp:761 +msgid "Must select a constraint with associated label." +msgstr "必须选择有尺寸的约束。" + +#: constraint.cpp:784 +msgid "" +"Bad selection for angle constraint. This constraint can apply to:\n" +"\n" +"Angle between:\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +"\n" +"Equal angles:\n" +" * four line segments or normals (equal angle between A,B and C,D)\n" +" * three line segments or normals (equal angle between A,B and B,C)\n" +msgstr "" +"角度约束的选择错误。此约束可用于:\n" +"\n" +" * 两条线段\n" +" * 两条法线\n" +" * 一条线段和一条法线\n" +"之间的角度\n" +" * 四条线段或法线(AB之间与CD之间角度相等)\n" +" * 三条线段或法线(AB之间与BC之间角度相等)\n" +"之间的角度相等" + +#: constraint.cpp:872 +msgid "Curve-curve tangency must apply in workplane." +msgstr "曲线相切必须在工作面内应用。" + +#: constraint.cpp:887 +msgid "" +"Bad selection for parallel / tangent constraint. This constraint can apply " +"to:\n" +"\n" +" * two faces\n" +" * two or more line segments (parallel)\n" +" * one or more line segments and one or more normals (parallel)\n" +" * two or more normals (parallel)\n" +" * two line segments, arcs, or beziers, that share an endpoint (tangent)\n" +" * two line segments, arcs, or beziers, that do not share an endpoint and " +"the end point(s) of the curve(s) (tangent)\n" +msgstr "" +"平行/相切约束的选择错误。此约束可用于:\n" +"\n" +" * 两个表面\n" +" * 两条以上线段(平行)\n" +" * 两条以上法线(平行)\n" +" * 一条以上线段与一条以上法线(平行)\n" +" * 有共同端点的两条弧/线段/贝塞尔曲线(相切)\n" +" * 无共同端点的两条线段/曲线/贝塞尔曲线, 及曲线的端点不共有 (相切)\n" + +#: constraint.cpp:914 +msgid "" +"Bad selection for perpendicular constraint. This constraint can apply to:\n" +"\n" +" * two faces\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +msgstr "" +"垂直约束的选择错误。此约束可用于:\n" +"\n" +" * 两个表面\n" +" * 两条线段\n" +" * 两条法线\n" +" * 一条线段与一条法线\n" + +#: constraint.cpp:931 +msgid "" +"Bad selection for lock point where dragged constraint. This constraint can " +"apply to:\n" +"\n" +" * a point\n" +msgstr "" +"定位后锁定点约束的选择错误。此约束可用于:\n" +"\n" +" * 一点\n" + +#: constraint.cpp:946 mouse.cpp:1160 +msgid "NEW COMMENT -- DOUBLE-CLICK TO EDIT" +msgstr "新备注——双击编辑" + +#: constraint.cpp:952 +msgid "click center of comment text" +msgstr "单击备注文本的中心" + +#: export.cpp:19 +msgid "" +"No solid model present; draw one with extrudes and revolves, or use Export " +"2d View to export bare lines and curves." +msgstr "" +"不存在实心体模型;用拉伸和扫略来绘制,或使用\"导出2D视图\"导出光杆直线和曲" +"线。" + +#: export.cpp:61 +msgid "" +"Bad selection for export section. Please select:\n" +"\n" +" * nothing, with an active workplane (workplane is section plane)\n" +" * a face (section plane through face)\n" +" * a point and two line segments (plane through point and parallel to " +"lines)\n" +msgstr "" +"导出截面的选择错误。请选择:\n" +"\n" +" * 空,有活动工作面时(工作面为截面)\n" +" * 一个表面(截面穿过表面)\n" +" * 一点与两条线段(截面穿过点并与线平行)\n" + +#: export.cpp:818 +msgid "Active group mesh is empty; nothing to export." +msgstr "活动组网格为空;没有能导出的。" + +#: exportvector.cpp:336 +msgid "freehand lines were replaced with continuous lines" +msgstr "手绘样式的线已替换为连续线" + +#: exportvector.cpp:338 +msgid "zigzag lines were replaced with continuous lines" +msgstr "锯齿样式的线已替换为连续线" + +#: exportvector.cpp:592 +msgid "" +"Some aspects of the drawing have no DXF equivalent and were not exported:\n" +msgstr "图纸的某些方面没有DXF等效项,并且未导出:\n" + +#: exportvector.cpp:838 +msgid "" +"PDF page size exceeds 200 by 200 inches; many viewers may reject this file." +msgstr "PDF页面大小超过200x200英寸;许多查看器可能会拒绝此文件。" + +#: file.cpp:44 group.cpp:91 +msgctxt "group-name" +msgid "sketch-in-plane" +msgstr "平面草图" + +#: file.cpp:62 +msgctxt "group-name" +msgid "#references" +msgstr "#参考" + +#: file.cpp:555 +msgid "The file is empty. It may be corrupt." +msgstr "文件为空,可能已损坏。" + +#: file.cpp:560 +msgid "" +"Unrecognized data in file. This file may be corrupt, or from a newer version " +"of the program." +msgstr "文件内数据无法识别。该文件可能损坏,或来自新版本程序。" + +#: file.cpp:876 +msgctxt "title" +msgid "Missing File" +msgstr "文件丢失" + +#: file.cpp:877 +#, c-format +msgctxt "dialog" +msgid "The linked file “%s” is not present." +msgstr "不存在连接的文件\"%s\"。" + +#: file.cpp:879 +msgctxt "dialog" +msgid "" +"Do you want to locate it manually?\n" +"\n" +"If you decline, any geometry that depends on the missing file will be " +"permanently removed." +msgstr "您是否想手动查找?如果拒绝,基于丢失文件的所有几何图形将被永久删除。" + +#: file.cpp:882 +msgctxt "button" +msgid "&Yes" +msgstr "是(&Y)" + +#: file.cpp:884 +msgctxt "button" +msgid "&No" +msgstr "否(&N)" + +#: file.cpp:886 solvespace.cpp:652 +msgctxt "button" +msgid "&Cancel" +msgstr "取消(&C)" + +#: graphicswin.cpp:41 +msgid "&File" +msgstr "文件(&F)" + +#: graphicswin.cpp:42 +msgid "&New" +msgstr "新建(&N)" + +#: graphicswin.cpp:43 +msgid "&Open..." +msgstr "打开...(&O)" + +#: graphicswin.cpp:44 +msgid "Open &Recent" +msgstr "打开最近文件(&R)" + +#: graphicswin.cpp:45 +msgid "&Save" +msgstr "保存(&S)" + +#: graphicswin.cpp:46 +msgid "Save &As..." +msgstr "另存为...(&A)" + +#: graphicswin.cpp:48 +msgid "Export &Image..." +msgstr "导出图片...(&I)" + +#: graphicswin.cpp:49 +msgid "Export 2d &View..." +msgstr "导出2D视图...(&V)" + +#: graphicswin.cpp:50 +msgid "Export 2d &Section..." +msgstr "导出2D截面...(&S)" + +#: graphicswin.cpp:51 +msgid "Export 3d &Wireframe..." +msgstr "导出3D线框...(&W)" + +#: graphicswin.cpp:52 +msgid "Export Triangle &Mesh..." +msgstr "导出三角网格...(&M)" + +#: graphicswin.cpp:53 +msgid "Export &Surfaces..." +msgstr "导出表面...(&S)" + +#: graphicswin.cpp:54 +msgid "Im&port..." +msgstr "导入...(&P)" + +#: graphicswin.cpp:57 +msgid "E&xit" +msgstr "退出(&E)" + +#: graphicswin.cpp:60 +msgid "&Edit" +msgstr "编辑(&E)" + +#: graphicswin.cpp:61 +msgid "&Undo" +msgstr "撤销(&U)" + +#: graphicswin.cpp:62 +msgid "&Redo" +msgstr "重做(&R)" + +#: graphicswin.cpp:63 +msgid "Re&generate All" +msgstr "全部重新生成(&G)" + +#: graphicswin.cpp:65 +msgid "Snap Selection to &Grid" +msgstr "吸附所选到格线(&G)" + +#: graphicswin.cpp:66 +msgid "Rotate Imported &90°" +msgstr "导入的模型旋转90°(&9)" + +#: graphicswin.cpp:68 +msgid "Cu&t" +msgstr "剪切(&T)" + +#: graphicswin.cpp:69 +msgid "&Copy" +msgstr "复制(&C)" + +#: graphicswin.cpp:70 +msgid "&Paste" +msgstr "粘贴(&P)" + +#: graphicswin.cpp:71 +msgid "Paste &Transformed..." +msgstr "粘贴并变换...(&T)" + +#: graphicswin.cpp:72 +msgid "&Delete" +msgstr "删除(&D)" + +#: graphicswin.cpp:74 +msgid "Select &Edge Chain" +msgstr "选择相连边(&E)" + +#: graphicswin.cpp:75 +msgid "Select &All" +msgstr "全选(&A)" + +#: graphicswin.cpp:76 +msgid "&Unselect All" +msgstr "全不选(&U)" + +#: graphicswin.cpp:78 +msgid "&Line Styles..." +msgstr "线的样式...(&L)" + +#: graphicswin.cpp:79 +msgid "&View Projection..." +msgstr "投影视图...(&V)" + +#: graphicswin.cpp:81 +msgid "Con&figuration..." +msgstr "配置...(&F)" + +#: graphicswin.cpp:84 +msgid "&View" +msgstr "查看(&V)" + +#: graphicswin.cpp:85 +msgid "Zoom &In" +msgstr "放大(&I)" + +#: graphicswin.cpp:86 +msgid "Zoom &Out" +msgstr "缩小(&O)" + +#: graphicswin.cpp:87 +msgid "Zoom To &Fit" +msgstr "适合窗口(&F)" + +#: graphicswin.cpp:89 +msgid "Align View to &Workplane" +msgstr "视图对齐至工作面(&W)" + +#: graphicswin.cpp:90 +msgid "Nearest &Ortho View" +msgstr "最接近的正交视图(&O)" + +#: graphicswin.cpp:91 +msgid "Nearest &Isometric View" +msgstr "最接近的等轴视图(&I)" + +#: graphicswin.cpp:92 +msgid "&Center View At Point" +msgstr "视图中心置于点(&C)" + +#: graphicswin.cpp:94 +msgid "Show Snap &Grid" +msgstr "显示吸附格线(&G)" + +#: graphicswin.cpp:95 +msgid "Darken Inactive Solids" +msgstr "非活动实心体变暗" + +#: graphicswin.cpp:96 +msgid "Use &Perspective Projection" +msgstr "使用透视投影(&P)" + +#: graphicswin.cpp:97 +msgid "Show E&xploded View" +msgstr "显示爆炸视图(&E)" + +#: graphicswin.cpp:98 +msgid "Dimension &Units" +msgstr "尺寸单位(&U)" + +#: graphicswin.cpp:99 +msgid "Dimensions in &Millimeters" +msgstr "毫米(&M)" + +#: graphicswin.cpp:100 +msgid "Dimensions in M&eters" +msgstr "ç±³(&E)" + +#: graphicswin.cpp:101 +msgid "Dimensions in &Inches" +msgstr "英寸(&I)" + +#: graphicswin.cpp:102 +msgid "Dimensions in &Feet and Inches" +msgstr "英尺和英寸(&F)" + +#: graphicswin.cpp:104 +msgid "Show &Toolbar" +msgstr "显示工具栏(&T)" + +#: graphicswin.cpp:105 +msgid "Show Property Bro&wser" +msgstr "显示属性浏览器(&W)" + +#: graphicswin.cpp:107 +msgid "&Full Screen" +msgstr "全屏(&F)" + +#: graphicswin.cpp:109 +msgid "&New Group" +msgstr "新建组(&N)" + +#: graphicswin.cpp:110 +msgid "Sketch In &3d" +msgstr "在三维空间绘制(&3)" + +#: graphicswin.cpp:111 +msgid "Sketch In New &Workplane" +msgstr "在新工作面绘制(&W)" + +#: graphicswin.cpp:113 +msgid "Step &Translating" +msgstr "步进平移(&T)" + +#: graphicswin.cpp:114 +msgid "Step &Rotating" +msgstr "步进旋转(&R)" + +#: graphicswin.cpp:116 +msgid "E&xtrude" +msgstr "挤出(&E)" + +#: graphicswin.cpp:117 +msgid "&Helix" +msgstr "螺旋(&H)" + +#: graphicswin.cpp:118 +msgid "&Lathe" +msgstr "转圈(&L)" + +#: graphicswin.cpp:119 +msgid "Re&volve" +msgstr "扫略(&V)" + +#: graphicswin.cpp:121 +msgid "Link / Assemble..." +msgstr "连接/装配..." + +#: graphicswin.cpp:122 +msgid "Link Recent" +msgstr "连接最近文件" + +#: graphicswin.cpp:124 +msgid "&Sketch" +msgstr "绘图(&S)" + +#: graphicswin.cpp:125 +msgid "In &Workplane" +msgstr "在工作面内(&W)" + +#: graphicswin.cpp:126 +msgid "Anywhere In &3d" +msgstr "在三维空间(&3)" + +#: graphicswin.cpp:128 +msgid "Datum &Point" +msgstr "基准点(&P)" + +#: graphicswin.cpp:129 +msgid "Wor&kplane" +msgstr "工作面(&k)" + +#: graphicswin.cpp:131 +msgid "Line &Segment" +msgstr "线段(&S)" + +#: graphicswin.cpp:132 +msgid "C&onstruction Line Segment" +msgstr "构造线线段(&C)" + +#: graphicswin.cpp:133 +msgid "&Rectangle" +msgstr "矩形(&R)" + +#: graphicswin.cpp:134 +msgid "&Circle" +msgstr "圆(&C)" + +#: graphicswin.cpp:135 +msgid "&Arc of a Circle" +msgstr "圆弧(&A)" + +#: graphicswin.cpp:136 +msgid "&Bezier Cubic Spline" +msgstr "三次贝塞尔样条(&B)" + +#: graphicswin.cpp:138 +msgid "&Text in TrueType Font" +msgstr "TrueType字体文本(&T)" + +#: graphicswin.cpp:139 +msgid "I&mage" +msgstr "图片(&m)" + +#: graphicswin.cpp:141 +msgid "To&ggle Construction" +msgstr "切换构造线(&G)" + +#: graphicswin.cpp:142 +msgid "Ta&ngent Arc at Point" +msgstr "点处创建内切弧(&n)" + +#: graphicswin.cpp:143 +msgid "Split Curves at &Intersection" +msgstr "交点处拆分曲线(&I)" + +#: graphicswin.cpp:145 +msgid "&Constrain" +msgstr "约束(&C)" + +#: graphicswin.cpp:146 +msgid "&Distance / Diameter" +msgstr "距离/直径(&D)" + +#: graphicswin.cpp:147 +msgid "Re&ference Dimension" +msgstr "参考尺寸(&F)" + +#: graphicswin.cpp:148 +msgid "A&ngle / Equal Angle" +msgstr "角度(&N)" + +#: graphicswin.cpp:149 +msgid "Reference An&gle" +msgstr "参考角度(&G)" + +#: graphicswin.cpp:150 +msgid "Other S&upplementary Angle" +msgstr "补角(&U)" + +#: graphicswin.cpp:151 +msgid "Toggle R&eference Dim" +msgstr "切换参考尺寸(&E)" + +#: graphicswin.cpp:153 +msgid "&Horizontal" +msgstr "æ°´å¹³(&H)" + +#: graphicswin.cpp:154 +msgid "&Vertical" +msgstr "竖直(&V)" + +#: graphicswin.cpp:156 +msgid "&On Point / Curve / Plane" +msgstr "在点/线/面上(&O)" + +#: graphicswin.cpp:157 +msgid "E&qual Length / Radius" +msgstr "长度/半径/角度相等(&Q)" + +#: graphicswin.cpp:158 +msgid "Length / Arc Ra&tio" +msgstr "(å¼§)长比例(&T)" + +#: graphicswin.cpp:159 +msgid "Length / Arc Diff&erence" +msgstr "(å¼§)长之差(&E)" + +#: graphicswin.cpp:160 +msgid "At &Midpoint" +msgstr "中点(&M)" + +#: graphicswin.cpp:161 +msgid "S&ymmetric" +msgstr "对称(&Y)" + +#: graphicswin.cpp:162 +msgid "Para&llel / Tangent" +msgstr "平行/相切(&L)" + +#: graphicswin.cpp:163 +msgid "&Perpendicular" +msgstr "垂直(&P)" + +#: graphicswin.cpp:164 +msgid "Same Orient&ation" +msgstr "同向(&A)" + +#: graphicswin.cpp:165 +msgid "Lock Point Where &Dragged" +msgstr "定位后锁定点(&D)" + +#: graphicswin.cpp:167 +msgid "Comment" +msgstr "备注" + +#: graphicswin.cpp:169 +msgid "&Analyze" +msgstr "分析(&A)" + +#: graphicswin.cpp:170 +msgid "Measure &Volume" +msgstr "测量体积(&V)" + +#: graphicswin.cpp:171 +msgid "Measure A&rea" +msgstr "测量面积(&R)" + +#: graphicswin.cpp:172 +msgid "Measure &Perimeter" +msgstr "测量周长(&P)" + +#: graphicswin.cpp:173 +msgid "Show &Interfering Parts" +msgstr "显示干涉部件(&I)" + +#: graphicswin.cpp:174 +msgid "Show &Naked Edges" +msgstr "显示裸露边(&N)" + +#: graphicswin.cpp:175 +msgid "Show &Center of Mass" +msgstr "显示质心(&C)" + +#: graphicswin.cpp:177 +msgid "Show &Underconstrained Points" +msgstr "显示欠约束的点(&U)" + +#: graphicswin.cpp:179 +msgid "&Trace Point" +msgstr "跟踪点(&T)" + +#: graphicswin.cpp:180 +msgid "&Stop Tracing..." +msgstr "停止跟踪...(&S)" + +#: graphicswin.cpp:181 +msgid "Step &Dimension..." +msgstr "尺寸步进...(&D)" + +#: graphicswin.cpp:183 +msgid "&Help" +msgstr "帮助(&H)" + +#: graphicswin.cpp:184 +msgid "&Language" +msgstr "语言(&L)" + +#: graphicswin.cpp:185 +msgid "&Website / Manual" +msgstr "网站/手册(&W)" + +#: graphicswin.cpp:186 +msgid "&Go to GitHub commit" +msgstr "转到Github commit(&G)" + +#: graphicswin.cpp:188 +msgid "&About" +msgstr "关于(&A)" + +#: graphicswin.cpp:362 +msgid "(no recent files)" +msgstr "(无最近文件)" + +#: graphicswin.cpp:370 +#, c-format +msgid "File '%s' does not exist." +msgstr "不存在文件\"%s\"。" + +#: graphicswin.cpp:779 +msgid "No workplane is active, so the grid will not appear." +msgstr "没有活动工作面,因此无法显示格线。" + +#: graphicswin.cpp:794 +msgid "" +"The perspective factor is set to zero, so the view will always be a parallel " +"projection.\n" +"\n" +"For a perspective projection, modify the perspective factor in the " +"configuration screen. A value around 0.3 is typical." +msgstr "" +"透视因数已设为零,因此视图将始终为平行投影。\n" +"\n" +"要使用透视投影,请在配置界面中修改透视因数。典型值大概是0.3。" + +#: graphicswin.cpp:884 +msgid "" +"Select a point; this point will become the center of the view on screen." +msgstr "选择一点; 此点将成为屏幕上视图的中心。" + +#: graphicswin.cpp:1193 +msgid "No additional entities share endpoints with the selected entities." +msgstr "所选物件不与其他物件有共同端点。" + +#: graphicswin.cpp:1211 +msgid "" +"To use this command, select a point or other entity from an linked part, or " +"make a link group the active group." +msgstr "" +"使用此命令时,选择连接来的部件上一点或其他成分,或将一个连接组设为活动组。" + +#: graphicswin.cpp:1234 +msgid "" +"No workplane is active. Activate a workplane (with Sketch -> In Workplane) " +"to define the plane for the snap grid." +msgstr "" +"无活动工作面。激活一个工作面(用\"绘图 -> 在工作面内\")以定义吸附格线所在的" +"平面。" + +#: graphicswin.cpp:1241 +msgid "" +"Can't snap these items to grid; select points, text comments, or constraints " +"with a label. To snap a line, select its endpoints." +msgstr "" +"不能吸附这些项目到格线;选择点、文本备注、有尺寸的约束。要吸附线请选择其端" +"点。" + +#: graphicswin.cpp:1326 +msgid "No workplane selected. Activating default workplane for this group." +msgstr "未选择工作面。将激活该组的默认工作面。" + +#: graphicswin.cpp:1329 +msgid "" +"No workplane is selected, and the active group does not have a default " +"workplane. Try selecting a workplane, or activating a sketch-in-new-" +"workplane group." +msgstr "" +"未选择工作面,且活动组没有默认工作面。请选择一个工作面,或激活一个\"平面草图" +"\"组。" + +#: graphicswin.cpp:1350 +msgid "" +"Bad selection for tangent arc at point. Select a single point, or select " +"nothing to set up arc parameters." +msgstr "\"点处创建内切弧\"的选择错误。选择单个点,或什么都不选来设置参数。" + +#: graphicswin.cpp:1361 +msgid "click point on arc (draws anti-clockwise)" +msgstr "点击弧上的点(逆时针绘制)" + +#: graphicswin.cpp:1362 +msgid "click to place datum point" +msgstr "点击放置基准点" + +#: graphicswin.cpp:1363 +msgid "click first point of line segment" +msgstr "点击线段的起点" + +#: graphicswin.cpp:1365 +msgid "click first point of construction line segment" +msgstr "点击构造线的起点" + +#: graphicswin.cpp:1366 +msgid "click first point of cubic segment" +msgstr "点击三次曲线段的起点" + +#: graphicswin.cpp:1367 +msgid "click center of circle" +msgstr "点击圆心" + +#: graphicswin.cpp:1368 +msgid "click origin of workplane" +msgstr "点击工作面原点" + +#: graphicswin.cpp:1369 +msgid "click one corner of rectangle" +msgstr "点击矩形的一个角" + +#: graphicswin.cpp:1370 +msgid "click top left of text" +msgstr "点击文本左上角" + +#: graphicswin.cpp:1376 +msgid "click top left of image" +msgstr "点击图片左上角" + +#: graphicswin.cpp:1402 +msgid "" +"No entities are selected. Select entities before trying to toggle their " +"construction state." +msgstr "未选中物件,切换构造状态前请先选中物件。" + +#: group.cpp:86 +msgctxt "group-name" +msgid "sketch-in-3d" +msgstr "三维草图" + +#: group.cpp:154 +msgid "" +"Bad selection for new sketch in workplane. This group can be created with:\n" +"\n" +" * a point (through the point, orthogonal to coordinate axes)\n" +" * a point and two line segments (through the point, parallel to the " +"lines)\n" +" * a point and a normal (through the point, orthogonal to the normal)\n" +" * a workplane (copy of the workplane)\n" +msgstr "" +"\"在新工作面绘制\"的选择错误。该组可创建以:\n" +"\n" +" * 一点(穿过该点,与坐标轴正交)\n" +" * 一点与两条线段(穿过点,平行于线)\n" +" * 一点和一条法线(穿过点,正交于法线)\n" +" * 工作面(副本)\n" + +#: group.cpp:170 +msgid "" +"Activate a workplane (Sketch -> In Workplane) before extruding. The sketch " +"will be extruded normal to the workplane." +msgstr "挤出前先激活工作面(绘图 -> 在工作面内),草图将沿工作面法线挤出。" + +#: group.cpp:179 +msgctxt "group-name" +msgid "extrude" +msgstr "挤出" + +#: group.cpp:184 +msgid "Lathe operation can only be applied to planar sketches." +msgstr "转圈操作只能用于平面草图。" + +#: group.cpp:195 +msgid "" +"Bad selection for new lathe group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"新建转圈组的选择错误,该组可创建以:\n" +"\n" +" * 一点与一条线段或法线(围绕通过点且平行于线的轴)\n" +" * 一条线段(围绕线段)\n" + +#: group.cpp:205 +msgctxt "group-name" +msgid "lathe" +msgstr "转圈" + +#: group.cpp:210 +msgid "Revolve operation can only be applied to planar sketches." +msgstr "扫略操作只能用于平面草图。" + +#: group.cpp:221 +msgid "" +"Bad selection for new revolve group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"新建扫略组的选择错误。该组可创建以:\n" +"\n" +" * 一点与线段/法线(围绕通过点且平行于线的轴旋转)\n" +" * 一条线段(围绕线段旋转)\n" + +#: group.cpp:233 +msgctxt "group-name" +msgid "revolve" +msgstr "扫略" + +#: group.cpp:238 +msgid "Helix operation can only be applied to planar sketches." +msgstr "螺旋操作只能用于平面草图。" + +#: group.cpp:249 +msgid "" +"Bad selection for new helix group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel " +"to line / normal, through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" +"新建螺旋组的选择错误。该组可创建以:\n" +"\n" +" * 一点与一条线段或法线(围绕通过点且平行于线的轴旋转)\n" +" * 线段(围绕线段旋转)\n" + +#: group.cpp:261 +msgctxt "group-name" +msgid "helix" +msgstr "螺旋" + +#: group.cpp:274 +msgid "" +"Bad selection for new rotation. This group can be created with:\n" +"\n" +" * a point, while locked in workplane (rotate in plane, about that " +"point)\n" +" * a point and a line or a normal (rotate about an axis through the " +"point, and parallel to line / normal)\n" +msgstr "" +"新建旋转组的选择错误。该组可创建以:\n" +"\n" +" * 一点,在工作面内新建时(在平面内围绕该点旋转)\n" +" * 一点与一条线段或法线(围绕通过点且平行于线的轴旋转)\n" + +#: group.cpp:287 +msgctxt "group-name" +msgid "rotate" +msgstr "旋转" + +#: group.cpp:298 +msgctxt "group-name" +msgid "translate" +msgstr "平移" + +#: group.cpp:422 +msgid "(unnamed)" +msgstr "(未命名)" + +#: groupmesh.cpp:710 +msgid "not closed contour, or not all same style!" +msgstr "轮廓未闭合,或样式不一致!" + +#: groupmesh.cpp:723 +msgid "points not all coplanar!" +msgstr "并非所有点都共面!" + +#: groupmesh.cpp:725 +msgid "contour is self-intersecting!" +msgstr "轮廓自相交!" + +#: groupmesh.cpp:727 +msgid "zero-length edge!" +msgstr "边长度为零!" + +#: importmesh.cpp:136 +msgid "Text-formated STL files are not currently supported" +msgstr "目前不支持文本格式的STL文件" + +#: modify.cpp:252 +msgid "Must be sketching in workplane to create tangent arc." +msgstr "必须在工作面内创建内切弧。" + +#: modify.cpp:299 +msgid "" +"To create a tangent arc, select a point where two non-construction lines or " +"circles in this group and workplane join." +msgstr "要创建内切弧,请选择活动组和工作面中,两个非构造线或圆连接处。" + +#: modify.cpp:386 +msgid "" +"Couldn't round this corner. Try a smaller radius, or try creating the " +"desired geometry by hand with tangency constraints." +msgstr "无法倒圆这个角。尝试更小的半径,或用切线约束手动创建。" + +#: modify.cpp:595 +msgid "Couldn't split this entity; lines, circles, or cubics only." +msgstr "不能拆分此物件;仅限直线、圆、三次曲线。" + +#: modify.cpp:622 +msgid "Must be sketching in workplane to split." +msgstr "必须在工作面内拆分。" + +#: modify.cpp:629 +msgid "" +"Select two entities that intersect each other (e.g. two lines/circles/arcs " +"or a line/circle/arc and a point)." +msgstr "选择两个相交的物件(例如两条线/圆/弧,或一条线/圆/弧与一点)。" + +#: modify.cpp:734 +msgid "Can't split; no intersection found." +msgstr "无法拆分;未发现交点。" + +#: mouse.cpp:558 +msgid "Assign to Style" +msgstr "指定样式" + +#: mouse.cpp:574 +msgid "No Style" +msgstr "无样式" + +#: mouse.cpp:577 +msgid "Newly Created Custom Style..." +msgstr "新建自定义样式..." + +#: mouse.cpp:584 +msgid "Group Info" +msgstr "组信息" + +#: mouse.cpp:604 +msgid "Style Info" +msgstr "样式信息" + +#: mouse.cpp:624 +msgid "Select Edge Chain" +msgstr "选择相连边" + +#: mouse.cpp:630 +msgid "Toggle Reference Dimension" +msgstr "切换参考尺寸" + +#: mouse.cpp:636 +msgid "Other Supplementary Angle" +msgstr "补角" + +#: mouse.cpp:641 +msgid "Snap to Grid" +msgstr "吸附至格线" + +#: mouse.cpp:650 +msgid "Remove Spline Point" +msgstr "删除样条点" + +#: mouse.cpp:685 +msgid "Add Spline Point" +msgstr "增加样条点" + +#: mouse.cpp:689 +msgid "Cannot add spline point: maximum number of points reached." +msgstr "无法增加样条点:超过点数限制。" + +#: mouse.cpp:714 +msgid "Toggle Construction" +msgstr "切换构造线" + +#: mouse.cpp:730 +msgid "Delete Point-Coincident Constraint" +msgstr "删除点点重合约束" + +#: mouse.cpp:748 +msgid "Cut" +msgstr "剪切" + +#: mouse.cpp:750 +msgid "Copy" +msgstr "复制" + +#: mouse.cpp:754 +msgid "Select All" +msgstr "全选" + +#: mouse.cpp:759 +msgid "Paste" +msgstr "粘贴" + +#: mouse.cpp:761 +msgid "Paste Transformed..." +msgstr "粘贴并变换..." + +#: mouse.cpp:766 +msgid "Delete" +msgstr "删除" + +#: mouse.cpp:769 +msgid "Unselect All" +msgstr "全不选" + +#: mouse.cpp:776 +msgid "Unselect Hovered" +msgstr "光标下的不选" + +#: mouse.cpp:785 +msgid "Zoom to Fit" +msgstr "适合窗口" + +#: mouse.cpp:987 mouse.cpp:1276 +msgid "click next point of line, or press Esc" +msgstr "点击下一个点,或按Esc取消" + +#: mouse.cpp:993 +msgid "" +"Can't draw rectangle in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "无法在三维空间绘制矩形;请先激活工作面(用\"绘图 -> 在工作面内\")。" + +#: mouse.cpp:1027 +msgid "click to place other corner of rectangle" +msgstr "点击放置矩形的另一个角" + +#: mouse.cpp:1048 +msgid "click to set radius" +msgstr "点击设置半径" + +#: mouse.cpp:1053 +msgid "" +"Can't draw arc in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "无法在三维空间绘制弧线,请激活工作面(用\"绘图 -> 在工作面内\")" + +#: mouse.cpp:1072 +msgid "click to place point" +msgstr "点击放置点" + +#: mouse.cpp:1088 +msgid "click next point of cubic, or press Esc" +msgstr "点击下一个点,或按Esc取消" + +#: mouse.cpp:1093 +msgid "" +"Sketching in a workplane already; sketch in 3d before creating new workplane." +msgstr "已在工作面内绘制;新建工作面之前先\"在三维空间绘制\"。" + +#: mouse.cpp:1109 +msgid "" +"Can't draw text in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "无法在三维空间绘制文本,请激活工作面(用\"绘图 -> 在工作面内\")" + +#: mouse.cpp:1126 +msgid "click to place bottom right of text" +msgstr "点击放置文本的右下角" + +#: mouse.cpp:1132 +msgid "" +"Can't draw image in 3d; first, activate a workplane with Sketch -> In " +"Workplane." +msgstr "无法在三维空间绘制图片,请激活工作面(用\"绘图 -> 在工作面内\")" + +#: platform/gui.cpp:85 platform/gui.cpp:90 solvespace.cpp:583 +msgctxt "file-type" +msgid "SolveSpace models" +msgstr "SolveSpace模型" + +#: platform/gui.cpp:89 +msgctxt "file-type" +msgid "ALL" +msgstr "全部" + +#: platform/gui.cpp:91 +msgctxt "file-type" +msgid "IDF circuit board" +msgstr "IDF电路图" + +#: platform/gui.cpp:92 +msgctxt "file-type" +msgid "STL triangle mesh" +msgstr "STL三角网格" + +#: platform/gui.cpp:96 +msgctxt "file-type" +msgid "PNG image" +msgstr "PNG图片" + +#: platform/gui.cpp:100 +msgctxt "file-type" +msgid "STL mesh" +msgstr "STL网格" + +#: platform/gui.cpp:101 +msgctxt "file-type" +msgid "Wavefront OBJ mesh" +msgstr "Wavefront obj网格" + +#: platform/gui.cpp:102 +msgctxt "file-type" +msgid "Three.js-compatible mesh, with viewer" +msgstr "Three.js兼容网格,带视图" + +#: platform/gui.cpp:103 +msgctxt "file-type" +msgid "Three.js-compatible mesh, mesh only" +msgstr "Three.js兼容网格,仅网格" + +#: platform/gui.cpp:104 +msgctxt "file-type" +msgid "VRML text file" +msgstr "VRML文本文件" + +#: platform/gui.cpp:108 platform/gui.cpp:115 platform/gui.cpp:122 +msgctxt "file-type" +msgid "STEP file" +msgstr "STEP文件" + +#: platform/gui.cpp:112 +msgctxt "file-type" +msgid "PDF file" +msgstr "PDF文件" + +#: platform/gui.cpp:113 +msgctxt "file-type" +msgid "Encapsulated PostScript" +msgstr "封装的PostScript" + +#: platform/gui.cpp:114 +msgctxt "file-type" +msgid "Scalable Vector Graphics" +msgstr "SVG矢量图" + +#: platform/gui.cpp:116 platform/gui.cpp:123 +msgctxt "file-type" +msgid "DXF file (AutoCAD 2007)" +msgstr "DXF文件(AutoCAD 2007)" + +#: platform/gui.cpp:117 +msgctxt "file-type" +msgid "HPGL file" +msgstr "HPGL文件" + +#: platform/gui.cpp:118 +msgctxt "file-type" +msgid "G Code" +msgstr "G代码" + +#: platform/gui.cpp:127 +msgctxt "file-type" +msgid "AutoCAD DXF and DWG files" +msgstr "AutoCAD DXF/DWG文件" + +#: platform/gui.cpp:131 +msgctxt "file-type" +msgid "Comma-separated values" +msgstr "逗号分隔数据" + +#: platform/guigtk.cpp:1434 platform/guimac.mm:1513 platform/guiwin.cpp:1641 +msgid "untitled" +msgstr "无标题" + +#: platform/guigtk.cpp:1445 platform/guigtk.cpp:1481 platform/guimac.mm:1471 +#: platform/guiwin.cpp:1639 +msgctxt "title" +msgid "Save File" +msgstr "保存文件" + +#: platform/guigtk.cpp:1446 platform/guigtk.cpp:1482 platform/guimac.mm:1454 +#: platform/guiwin.cpp:1645 +msgctxt "title" +msgid "Open File" +msgstr "打开文件" + +#: platform/guigtk.cpp:1449 platform/guigtk.cpp:1488 +msgctxt "button" +msgid "_Cancel" +msgstr "取消(_C)" + +#: platform/guigtk.cpp:1450 platform/guigtk.cpp:1486 +msgctxt "button" +msgid "_Save" +msgstr "保存(_S)" + +#: platform/guigtk.cpp:1451 platform/guigtk.cpp:1487 +msgctxt "button" +msgid "_Open" +msgstr "打开(_O)" + +#: solvespace.cpp:175 +msgctxt "title" +msgid "Autosave Available" +msgstr "自动保存可用" + +#: solvespace.cpp:176 +msgctxt "dialog" +msgid "An autosave file is available for this sketch." +msgstr "该草图有自动保存文件可用。" + +#: solvespace.cpp:177 +msgctxt "dialog" +msgid "Do you want to load the autosave file instead?" +msgstr "是否加载自动保存文件?" + +#: solvespace.cpp:178 +msgctxt "button" +msgid "&Load autosave" +msgstr "加载(&L)" + +#: solvespace.cpp:180 +msgctxt "button" +msgid "Do&n't Load" +msgstr "不加载(&N)" + +#: solvespace.cpp:640 +msgctxt "title" +msgid "Modified File" +msgstr "文件已修改" + +#: solvespace.cpp:642 +#, c-format +msgctxt "dialog" +msgid "Do you want to save the changes you made to the sketch “%s”?" +msgstr "" +"是否保存您对草图\n" +"“%s”\n" +"的修改?" + +#: solvespace.cpp:645 +msgctxt "dialog" +msgid "Do you want to save the changes you made to the new sketch?" +msgstr "是否保存您对新草图的修改?" + +#: solvespace.cpp:648 +msgctxt "dialog" +msgid "Your changes will be lost if you don't save them." +msgstr "如果不保存,您的修改将会丢失。" + +#: solvespace.cpp:649 +msgctxt "button" +msgid "&Save" +msgstr "保存(&S)" + +#: solvespace.cpp:651 +msgctxt "button" +msgid "Do&n't Save" +msgstr "不保存(&N)" + +#: solvespace.cpp:672 +msgctxt "title" +msgid "(new sketch)" +msgstr "(新草图)" + +#: solvespace.cpp:683 +msgctxt "title" +msgid "Property Browser" +msgstr "属性浏览器" + +#: solvespace.cpp:746 +msgid "" +"Constraints are currently shown, and will be exported in the toolpath. This " +"is probably not what you want; hide them by clicking the link at the top of " +"the text window." +msgstr "" +"当前显示约束,并将导出至刀具路径。这可能不是你想要的;点击文本窗口顶部的按钮" +"来隐藏它们。" + +#: solvespace.cpp:834 +#, c-format +msgid "" +"Can't identify file type from file extension of filename '%s'; try .dxf or ." +"dwg." +msgstr "无法从'%s'的文件扩展名识别文件类型,请尝试.dxf或.dwg。" + +#: solvespace.cpp:886 +msgid "Constraint must have a label, and must not be a reference dimension." +msgstr "约束必须有尺寸,且不能是参考尺寸。" + +#: solvespace.cpp:890 +msgid "Bad selection for step dimension; select a constraint." +msgstr "尺寸步进选择错误,请选择约束。" + +#: solvespace.cpp:914 +msgid "The assembly does not interfere, good." +msgstr "装配无干涉,好。" + +#: solvespace.cpp:930 +#, c-format +msgid "" +"The volume of the solid model is:\n" +"\n" +" %s" +msgstr "" +"实心体模型的体积是:\n" +"\n" +" %s" + +#: solvespace.cpp:939 +#, c-format +msgid "" +"\n" +"The volume of current group mesh is:\n" +"\n" +" %s" +msgstr "" +"\n" +"当前组的网格体积是:\n" +"\n" +" %s" + +#: solvespace.cpp:944 +msgid "" +"\n" +"\n" +"Curved surfaces have been approximated as triangles.\n" +"This introduces error, typically of around 1%." +msgstr "" +"\n" +"\n" +"曲面已近似为三角形。这会引入误差,通常约为1%。" + +#: solvespace.cpp:959 +#, c-format +msgid "" +"The surface area of the selected faces is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"所选表面的表面积为:\n" +"\n" +" %s\n" +"\n" +"曲线已近似为线性分段。这会引入误差,通常约为1%%。" + +#: solvespace.cpp:968 +msgid "" +"This group does not contain a correctly-formed 2d closed area. It is open, " +"not coplanar, or self-intersecting." +msgstr "此组不包含正确形式的二维封闭区域。它可能是开放的,或不共面,或自相交。" + +#: solvespace.cpp:980 +#, c-format +msgid "" +"The area of the region sketched in this group is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"该组中所画区域的面积为:\n" +"\n" +" %s\n" +"\n" +"曲线已近似为线性分段。这会引入误差,通常约为1%%。" + +#: solvespace.cpp:1000 +#, c-format +msgid "" +"The total length of the selected entities is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" +"所选物件的总长度为:\n" +"\n" +" %s\n" +"\n" +"曲线已近似为线性分段。这会引入误差,通常约为1%%。" + +#: solvespace.cpp:1006 +msgid "Bad selection for perimeter; select line segments, arcs, and curves." +msgstr "周长选择错误;请选择线段、弧线、曲线。" + +#: solvespace.cpp:1022 +msgid "Bad selection for trace; select a single point." +msgstr "跟踪选择不当;请选择单个点。" + +#: solvespace.cpp:1049 +#, c-format +msgid "Couldn't write to '%s'" +msgstr "无法写入'%s'" + +#: solvespace.cpp:1079 +msgid "The mesh is self-intersecting (NOT okay, invalid)." +msgstr "网格是自相交的(不行,无效)" + +#: solvespace.cpp:1080 +msgid "The mesh is not self-intersecting (okay, valid)." +msgstr "网格不是自相交的(行,有效)" + +#: solvespace.cpp:1082 +msgid "The mesh has naked edges (NOT okay, invalid)." +msgstr "网格有裸露边(不行,无效)" + +#: solvespace.cpp:1083 +msgid "The mesh is watertight (okay, valid)." +msgstr "网格是水密的(行,有效)" + +#: solvespace.cpp:1086 +#, c-format +msgid "" +"\n" +"\n" +"The model contains %d triangles, from %d surfaces." +msgstr "" +"\n" +"\n" +"该模型包含%d个表面上的%d个三角形。" + +#: solvespace.cpp:1090 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"Zero problematic edges, good.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"没有问题边,好。%s" + +#: solvespace.cpp:1093 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d problematic edges, bad.%s" +msgstr "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d条边有问题,不好。%s" + +#: solvespace.cpp:1106 +#, c-format +msgid "" +"This is SolveSpace version %s.\n" +"\n" +"For more information, see http://solvespace.com/\n" +"\n" +"SolveSpace is free software: you are free to modify\n" +"and/or redistribute it under the terms of the GNU\n" +"General Public License (GPL) version 3 or later.\n" +"\n" +"There is NO WARRANTY, to the extent permitted by\n" +"law. For details, visit http://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues and other authors.\n" +msgstr "" +"当前SolveSpace版本 %s\n" +"\n" +"更多信息请见 https://solvespace.com/\n" +"\n" +"SolveSpace是自由软件:您可以自由修改或\n" +"重新发行,但需遵循GNU通用公共许可协议\n" +"(GNU General Public License, GPL) 第3版\n" +"或后续版本。\n" +"\n" +"在法律允许范围内不包含质保,详见\n" +"https://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues等作者\n" + +#: style.cpp:185 +msgid "" +"Can't assign style to an entity that's derived from another entity; try " +"assigning a style to this entity's parent." +msgstr "无法将样式分配给派生自其他物件的物件;尝试将样式分配给此物件的父级。" + +#: style.cpp:735 +msgid "Style name cannot be empty" +msgstr "样式名称不能为空" + +#: textscreens.cpp:837 +msgid "Can't repeat fewer than 1 time." +msgstr "不能重复 1 次以下。" + +#: textscreens.cpp:841 +msgid "Can't repeat more than 999 times." +msgstr "不能重复 999 次以上。" + +#: textscreens.cpp:866 +msgid "Group name cannot be empty" +msgstr "组名不能为空" + +#: textscreens.cpp:918 +msgid "Opacity must be between zero and one." +msgstr "不透明度必须在 0 到 1 之间。" + +#: textscreens.cpp:953 +msgid "Radius cannot be zero or negative." +msgstr "半径偏移不能为负数。" + +#: toolbar.cpp:18 +msgid "Sketch line segment" +msgstr "绘制线段" + +#: toolbar.cpp:20 +msgid "Sketch rectangle" +msgstr "绘制矩形" + +#: toolbar.cpp:22 +msgid "Sketch circle" +msgstr "绘制圆" + +#: toolbar.cpp:24 +msgid "Sketch arc of a circle" +msgstr "绘制圆弧" + +#: toolbar.cpp:26 +msgid "Sketch curves from text in a TrueType font" +msgstr "绘制TrueType字体文本曲线" + +#: toolbar.cpp:28 +msgid "Sketch image from a file" +msgstr "绘制图像文件" + +#: toolbar.cpp:30 +msgid "Create tangent arc at selected point" +msgstr "选定点处创建内切弧" + +#: toolbar.cpp:32 +msgid "Sketch cubic Bezier spline" +msgstr "绘制三次贝塞尔样条" + +#: toolbar.cpp:34 +msgid "Sketch datum point" +msgstr "绘制基准点" + +#: toolbar.cpp:36 +msgid "Toggle construction" +msgstr "切换构造线" + +#: toolbar.cpp:38 +msgid "Split lines / curves where they intersect" +msgstr "交点处拆分(曲)线" + +#: toolbar.cpp:42 +msgid "Constrain distance / diameter / length" +msgstr "约束距离/直径/长度" + +#: toolbar.cpp:44 +msgid "Constrain angle" +msgstr "约束角度" + +#: toolbar.cpp:46 +msgid "Constrain to be horizontal" +msgstr "约束为水平" + +#: toolbar.cpp:48 +msgid "Constrain to be vertical" +msgstr "约束为竖直" + +#: toolbar.cpp:50 +msgid "Constrain to be parallel or tangent" +msgstr "约束为平行或相切" + +#: toolbar.cpp:52 +msgid "Constrain to be perpendicular" +msgstr "约束为垂直" + +#: toolbar.cpp:54 +msgid "Constrain point on line / curve / plane / point" +msgstr "约束点在直线/曲线/面/点上" + +#: toolbar.cpp:56 +msgid "Constrain symmetric" +msgstr "约束为对称" + +#: toolbar.cpp:58 +msgid "Constrain equal length / radius / angle" +msgstr "约束长度/半径/角度相等" + +#: toolbar.cpp:60 +msgid "Constrain normals in same orientation" +msgstr "约束法线同向" + +#: toolbar.cpp:62 +msgid "Other supplementary angle" +msgstr "补角" + +#: toolbar.cpp:64 +msgid "Toggle reference dimension" +msgstr "切换参考尺寸" + +#: toolbar.cpp:68 +msgid "New group extruding active sketch" +msgstr "新组:活动草图挤出" + +#: toolbar.cpp:70 +msgid "New group rotating active sketch" +msgstr "新组:活动草图旋转" + +#: toolbar.cpp:72 +msgid "New group helix from active sketch" +msgstr "新组:活动草图螺旋" + +#: toolbar.cpp:74 +msgid "New group revolve active sketch" +msgstr "新组:活动草图扫略" + +#: toolbar.cpp:76 +msgid "New group step and repeat rotating" +msgstr "新组:步进旋转" + +#: toolbar.cpp:78 +msgid "New group step and repeat translating" +msgstr "新组:步进平移" + +#: toolbar.cpp:80 +msgid "New group in new workplane (thru given entities)" +msgstr "新组:在新工作面绘制(由给定物件指定)" + +#: toolbar.cpp:82 +msgid "New group in 3d" +msgstr "新组:在三维空间绘制" + +#: toolbar.cpp:84 +msgid "New group linking / assembling file" +msgstr "新组:连接/装配文件" + +#: toolbar.cpp:88 +msgid "Nearest isometric view" +msgstr "最接近的等轴视图" + +#: toolbar.cpp:90 +msgid "Align view to active workplane" +msgstr "视图对齐至工作面" + +#: util.cpp:165 +msgctxt "title" +msgid "Error" +msgstr "错误" + +#: util.cpp:165 +msgctxt "title" +msgid "Message" +msgstr "消息" + +#: util.cpp:170 +msgctxt "button" +msgid "&OK" +msgstr "好(&O)" + +#: view.cpp:127 +msgid "Scale cannot be zero or negative." +msgstr "缩放不能为零。" + +#: view.cpp:139 view.cpp:148 +msgid "Bad format: specify x, y, z" +msgstr "格式错误:请指定 x,y,z" + +#~ msgid "" +#~ "Bad selection for on point / curve / plane constraint. This constraint " +#~ "can apply to:\n" +#~ "\n" +#~ " * two points (points coincident)\n" +#~ " * a point and a workplane (point in plane)\n" +#~ " * a point and a line segment (point on line)\n" +#~ " * a point and a circle or arc (point on curve)\n" +#~ " * a point and a plane face (point on face)\n" +#~ msgstr "" +#~ "点 / 曲线 / 平面约束的选定方法错误。此约束可应用于:\n" +#~ "\n" +#~ "* 两点(点重合)\n" +#~ " * 一个点和一个工作面(平面中点)\n" +#~ " * 点和线段(点在线)\n" +#~ " * 一个点和一个圆或圆(曲线上的点)\n" +#~ " * 点和平面面(点在脸上)\n" + +#~ msgid "" +#~ "Bad selection for equal length / radius constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two line segments (equal length)\n" +#~ " * two line segments and two points (equal point-line distances)\n" +#~ " * a line segment and two points (equal point-line distances)\n" +#~ " * a line segment, and a point and line segment (point-line distance " +#~ "equals length)\n" +#~ " * four line segments or normals (equal angle between A,B and C,D)\n" +#~ " * three line segments or normals (equal angle between A,B and B,C)\n" +#~ " * two circles or arcs (equal radius)\n" +#~ " * a line segment and an arc (line segment length equals arc length)\n" +#~ msgstr "" +#~ "等长度/半径约束的选定方法错误。此约束可应用于:\n" +#~ "\n" +#~ "* 两个线段(相等长度)\n" +#~ " * 两个线段和两个点(相等的点线距离)\n" +#~ " * 线段和两个点(相等的点线距离)\n" +#~ " * 线段和点段和线段(点线距离等于长度)\n" +#~ " * 四条线段或法线(A、B 和 C、D 之间的等角)\n" +#~ " * 三条线段或法线(A、B 和 B、C 之间的等角)\n" +#~ " * 两个圆或圆(相等半径)\n" +#~ " * 线段和圆弧(线段长度等于弧长)\n" + +#~ msgid "" +#~ "Bad selection for horizontal / vertical constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two points\n" +#~ " * a line segment\n" +#~ msgstr "" +#~ "æ°´å¹³/竖直约束的选择错误。此约束可应用于:\n" +#~ "\n" +#~ " * 两点\n" +#~ " * 线段\n" + +#~ msgid "" +#~ "Bad selection for angle constraint. This constraint can apply to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ " * a line segment and a normal\n" +#~ " * two normals\n" +#~ msgstr "" +#~ "角度约束的选择错误。此约束可应用于:\n" +#~ "\n" +#~ "* 两个线段\n" +#~ " * 线段和法线\n" +#~ " * 两个法线\n" + +#~ msgid "" +#~ "Bad selection for parallel / tangent constraint. This constraint can " +#~ "apply to:\n" +#~ "\n" +#~ " * two line segments (parallel)\n" +#~ " * a line segment and a normal (parallel)\n" +#~ " * two normals (parallel)\n" +#~ " * two line segments, arcs, or beziers, that share an endpoint " +#~ "(tangent)\n" +#~ msgstr "" +#~ "平行/切线约束的选择错误。此约束可应用于:\n" +#~ "\n" +#~ "* 两条线段(平行)\n" +#~ " * 线段和法线(平行)\n" +#~ " * 两个法线(平行)\n" +#~ " * 有共同端点的两条线段、弧线或贝塞尔(切线)\n" + +#~ msgid "" +#~ "Bad selection for perpendicular constraint. This constraint can apply " +#~ "to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ " * a line segment and a normal\n" +#~ " * two normals\n" +#~ msgstr "" +#~ "垂直约束的选择错误。此约束可应用于:\n" +#~ "\n" +#~ "* 两个线段\n" +#~ " * 线段和法线\n" +#~ " * 两个法线\n" + +#~ msgid "A&ngle" +#~ msgstr "角度(&A)" + +#~ msgid "E&qual Length / Radius / Angle" +#~ msgstr "等于/长度/半径/角度(&Q)" + +#~ msgid "" +#~ "Bad selection for length ratio constraint. This constraint can apply to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ msgstr "" +#~ "长度比例约束的选择错误。此约束可应用于:\n" +#~ "\n" +#~ "* 两个线段\n" + +#~ msgid "" +#~ "Bad selection for length difference constraint. This constraint can apply " +#~ "to:\n" +#~ "\n" +#~ " * two line segments\n" +#~ msgstr "" +#~ "长度差异约束的选择错误。此约束可应用于:\n" +#~ "\n" +#~ "* 两个线段\n" + +#~ msgid "Length Ra&tio" +#~ msgstr "长度比例(&T)" + +#~ msgid "Length Diff&erence" +#~ msgstr "长度偏差(&E)" + +#~ msgid "" +#~ "Bad selection for new sketch in workplane. This group can be created " +#~ "with:\n" +#~ "\n" +#~ " * a point (through the point, orthogonal to coordinate axes)\n" +#~ " * a point and two line segments (through the point, parallel to the " +#~ "lines)\n" +#~ " * a workplane (copy of the workplane)\n" +#~ msgstr "" +#~ "在新工作面内绘图选择失败,该组可以使用:\n" +#~ "\n" +#~ " * 一个点(通过该点,正交至坐标轴)\n" +#~ " * 一个点和二个线段(通过点,绘制平行线至线段)\n" +#~ " * 一个工作面(复制工作面)\n" + +#~ msgctxt "file-type" +#~ msgid "Q3D Object file" +#~ msgstr "Q3D对象文件" diff --git a/res/messages.pot b/res/messages.pot new file mode 100644 index 0000000..4e51fda --- /dev/null +++ b/res/messages.pot @@ -0,0 +1,1980 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR the PACKAGE authors +# This file is distributed under the same license as the SolveSpace package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: SolveSpace 3.1\n" +"Report-Msgid-Bugs-To: phkahler@gmail.com\n" +"POT-Creation-Date: 2025-01-26 21:04+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: clipboard.cpp:314 +msgid "" +"Cut, paste, and copy work only in a workplane.\n" +"\n" +"Activate one with Sketch -> In Workplane." +msgstr "" + +#: clipboard.cpp:331 +msgid "Clipboard is empty; nothing to paste." +msgstr "" + +#: clipboard.cpp:378 +msgid "Number of copies to paste must be at least one." +msgstr "" + +#: clipboard.cpp:394 textscreens.cpp:879 +msgid "Scale cannot be zero." +msgstr "" + +#: clipboard.cpp:436 +msgid "Select one point to define origin of rotation." +msgstr "" + +#: clipboard.cpp:448 +msgid "Select two points to define translation vector." +msgstr "" + +#: clipboard.cpp:458 +msgid "Transformation is identity. So all copies will be exactly on top of each other." +msgstr "" + +#: clipboard.cpp:462 +msgid "Too many items to paste; split this into smaller pastes." +msgstr "" + +#: clipboard.cpp:467 +msgid "No workplane active." +msgstr "" + +#: confscreen.cpp:410 +msgid "Bad format: specify coordinates as x, y, z" +msgstr "" + +#: confscreen.cpp:420 style.cpp:729 textscreens.cpp:910 +msgid "Bad format: specify color as r, g, b" +msgstr "" + +#: confscreen.cpp:446 +msgid "" +"The perspective factor will have no effect until you enable View -> Use Perspective Projection." +msgstr "" + +#: confscreen.cpp:464 confscreen.cpp:474 +#, c-format +msgid "Specify between 0 and %d digits after the decimal." +msgstr "" + +#: confscreen.cpp:486 +msgid "Export scale must not be zero!" +msgstr "" + +#: confscreen.cpp:498 +msgid "Cutter radius offset must not be negative!" +msgstr "" + +#: confscreen.cpp:557 +msgid "Bad value: autosave interval should be positive" +msgstr "" + +#: confscreen.cpp:560 +msgid "Bad format: specify interval in integral minutes" +msgstr "" + +#: constraint.cpp:12 +msgctxt "constr-name" +msgid "pts-coincident" +msgstr "" + +#: constraint.cpp:13 +msgctxt "constr-name" +msgid "pt-pt-distance" +msgstr "" + +#: constraint.cpp:14 +msgctxt "constr-name" +msgid "pt-line-distance" +msgstr "" + +#: constraint.cpp:15 +msgctxt "constr-name" +msgid "pt-plane-distance" +msgstr "" + +#: constraint.cpp:16 +msgctxt "constr-name" +msgid "pt-face-distance" +msgstr "" + +#: constraint.cpp:17 +msgctxt "constr-name" +msgid "proj-pt-pt-distance" +msgstr "" + +#: constraint.cpp:18 +msgctxt "constr-name" +msgid "pt-in-plane" +msgstr "" + +#: constraint.cpp:19 +msgctxt "constr-name" +msgid "pt-on-line" +msgstr "" + +#: constraint.cpp:20 +msgctxt "constr-name" +msgid "pt-on-face" +msgstr "" + +#: constraint.cpp:21 +msgctxt "constr-name" +msgid "eq-length" +msgstr "" + +#: constraint.cpp:22 +msgctxt "constr-name" +msgid "eq-length-and-pt-ln-dist" +msgstr "" + +#: constraint.cpp:23 +msgctxt "constr-name" +msgid "eq-pt-line-distances" +msgstr "" + +#: constraint.cpp:24 +msgctxt "constr-name" +msgid "length-ratio" +msgstr "" + +#: constraint.cpp:25 +msgctxt "constr-name" +msgid "arc-arc-length-ratio" +msgstr "" + +#: constraint.cpp:26 +msgctxt "constr-name" +msgid "arc-line-length-ratio" +msgstr "" + +#: constraint.cpp:27 +msgctxt "constr-name" +msgid "length-difference" +msgstr "" + +#: constraint.cpp:28 +msgctxt "constr-name" +msgid "arc-arc-len-difference" +msgstr "" + +#: constraint.cpp:29 +msgctxt "constr-name" +msgid "arc-line-len-difference" +msgstr "" + +#: constraint.cpp:30 +msgctxt "constr-name" +msgid "symmetric" +msgstr "" + +#: constraint.cpp:31 +msgctxt "constr-name" +msgid "symmetric-h" +msgstr "" + +#: constraint.cpp:32 +msgctxt "constr-name" +msgid "symmetric-v" +msgstr "" + +#: constraint.cpp:33 +msgctxt "constr-name" +msgid "symmetric-line" +msgstr "" + +#: constraint.cpp:34 +msgctxt "constr-name" +msgid "at-midpoint" +msgstr "" + +#: constraint.cpp:35 +msgctxt "constr-name" +msgid "horizontal" +msgstr "" + +#: constraint.cpp:36 +msgctxt "constr-name" +msgid "vertical" +msgstr "" + +#: constraint.cpp:37 +msgctxt "constr-name" +msgid "diameter" +msgstr "" + +#: constraint.cpp:38 +msgctxt "constr-name" +msgid "pt-on-circle" +msgstr "" + +#: constraint.cpp:39 +msgctxt "constr-name" +msgid "same-orientation" +msgstr "" + +#: constraint.cpp:40 +msgctxt "constr-name" +msgid "angle" +msgstr "" + +#: constraint.cpp:41 +msgctxt "constr-name" +msgid "parallel" +msgstr "" + +#: constraint.cpp:42 +msgctxt "constr-name" +msgid "arc-line-tangent" +msgstr "" + +#: constraint.cpp:43 +msgctxt "constr-name" +msgid "cubic-line-tangent" +msgstr "" + +#: constraint.cpp:44 +msgctxt "constr-name" +msgid "curve-curve-tangent" +msgstr "" + +#: constraint.cpp:45 +msgctxt "constr-name" +msgid "perpendicular" +msgstr "" + +#: constraint.cpp:46 +msgctxt "constr-name" +msgid "eq-radius" +msgstr "" + +#: constraint.cpp:47 +msgctxt "constr-name" +msgid "eq-angle" +msgstr "" + +#: constraint.cpp:48 +msgctxt "constr-name" +msgid "eq-line-len-arc-len" +msgstr "" + +#: constraint.cpp:49 +msgctxt "constr-name" +msgid "lock-where-dragged" +msgstr "" + +#: constraint.cpp:50 +msgctxt "constr-name" +msgid "comment" +msgstr "" + +#: constraint.cpp:151 +msgid "" +"The point you selected does not belong to the arc. The arc and line segment do not share an end " +"point.\n" +"\n" +"Select the end point of the arc at which you want it to be tangent to the line." +msgstr "" + +#: constraint.cpp:158 +msgid "" +"The tangent arc and line segment must share an endpoint. Constrain them with Constrain -> On " +"Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the arc at which you want it to be tangent to the line." +msgstr "" + +#: constraint.cpp:186 +msgid "" +"The point you selected is not an end point of the cubic spline. The spline and line segment do " +"not share an end point.\n" +"\n" +"Select the end point of the spline at which you want it to be tangent to the line." +msgstr "" + +#: constraint.cpp:193 +msgid "" +"The tangent cubic spline and line segment must share an endpoint. Constrain them with Constrain -" +"> On Point before constraining tangent.\n" +"\n" +"Alternatively select the end point of the cubic spline at which you want it to be tangent to the " +"line." +msgstr "" + +#: constraint.cpp:237 +msgid "" +"The points you selected are not end points of the two curves. The curves do not share an end " +"point.\n" +"\n" +"Select the end points of both curves at which you want them to be tangent to each other." +msgstr "" + +#: constraint.cpp:244 +msgid "" +"The curves must share an endpoint. Constrain them with Constrain -> On Point before constraining " +"tangent.\n" +"\n" +"Alternatively select the end points of both curves at which you want the curves to be tangent." +msgstr "" + +#: constraint.cpp:303 +msgid "" +"Bad selection for distance / diameter constraint. This constraint can apply to:\n" +"\n" +" * two points (distance between points)\n" +" * a line segment (length)\n" +" * two points and a line segment or normal (projected distance)\n" +" * a workplane and a point (minimum distance)\n" +" * a line segment and a point (minimum distance)\n" +" * a plane face and a point (minimum distance)\n" +" * a circle or an arc (diameter)\n" +msgstr "" + +#: constraint.cpp:366 +msgid "" +"Bad selection for on point / curve / plane constraint. This constraint can apply to:\n" +"\n" +" * two or more points (points coincident)\n" +" * a point and a workplane (point in plane)\n" +" * a point and a line segment (point on line)\n" +" * a point and a circle or arc (point on curve)\n" +" * a point and one to three plane faces (point on face(s))\n" +msgstr "" + +#: constraint.cpp:427 +msgid "" +"Bad selection for equal length / radius constraint. This constraint can apply to:\n" +"\n" +" * two or more line segments (equal length)\n" +" * two line segments and two points (equal point-line distances)\n" +" * a line segment and two points (equal point-line distances)\n" +" * a line segment, and a point and line segment (point-line distance equals length)\n" +" * two or more circles or arcs (equal radius)\n" +" * a line segment and an arc (line segment length equals arc length)\n" +msgstr "" + +#: constraint.cpp:480 +msgid "" +"Bad selection for length ratio constraint. This constraint can apply to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" + +#: constraint.cpp:515 +msgid "" +"Bad selection for length difference constraint. This constraint can apply to:\n" +"\n" +" * two line segments\n" +" * two arcs\n" +" * one arc and one line segment\n" +msgstr "" + +#: constraint.cpp:550 +msgid "" +"Bad selection for at midpoint constraint. This constraint can apply to:\n" +"\n" +" * a line segment and a point (point at midpoint)\n" +" * a line segment and a workplane (line's midpoint on plane)\n" +msgstr "" + +#: constraint.cpp:608 +msgid "" +"Bad selection for symmetric constraint. This constraint can apply to:\n" +"\n" +" * two points or a line segment (symmetric about workplane's coordinate axis)\n" +" * line segment, and two points or a line segment (symmetric about line segment)\n" +" * workplane, and two points or a line segment (symmetric about workplane)\n" +msgstr "" + +#: constraint.cpp:623 +msgid "A workplane must be active when constraining symmetric without an explicit symmetry plane." +msgstr "" + +#: constraint.cpp:663 +msgid "" +"Activate a workplane (with Sketch -> In Workplane) before applying a horizontal or vertical " +"constraint." +msgstr "" + +#: constraint.cpp:679 +msgid "" +"Bad selection for horizontal / vertical constraint. This constraint can apply to:\n" +"\n" +" * two or more points\n" +" * one or more line segments\n" +msgstr "" + +#: constraint.cpp:697 +msgid "" +"Bad selection for same orientation constraint. This constraint can apply to:\n" +"\n" +" * two normals\n" +msgstr "" + +#: constraint.cpp:748 +msgid "Must select an angle constraint." +msgstr "" + +#: constraint.cpp:761 +msgid "Must select a constraint with associated label." +msgstr "" + +#: constraint.cpp:784 +msgid "" +"Bad selection for angle constraint. This constraint can apply to:\n" +"\n" +"Angle between:\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +"\n" +"Equal angles:\n" +" * four line segments or normals (equal angle between A,B and C,D)\n" +" * three line segments or normals (equal angle between A,B and B,C)\n" +msgstr "" + +#: constraint.cpp:872 +msgid "Curve-curve tangency must apply in workplane." +msgstr "" + +#: constraint.cpp:887 +msgid "" +"Bad selection for parallel / tangent constraint. This constraint can apply to:\n" +"\n" +" * two faces\n" +" * two or more line segments (parallel)\n" +" * one or more line segments and one or more normals (parallel)\n" +" * two or more normals (parallel)\n" +" * two line segments, arcs, or beziers, that share an endpoint (tangent)\n" +" * two line segments, arcs, or beziers, that do not share an endpoint and the end point(s) of " +"the curve(s) (tangent)\n" +msgstr "" + +#: constraint.cpp:914 +msgid "" +"Bad selection for perpendicular constraint. This constraint can apply to:\n" +"\n" +" * two faces\n" +" * two line segments\n" +" * a line segment and a normal\n" +" * two normals\n" +msgstr "" + +#: constraint.cpp:931 +msgid "" +"Bad selection for lock point where dragged constraint. This constraint can apply to:\n" +"\n" +" * a point\n" +msgstr "" + +#: constraint.cpp:946 mouse.cpp:1160 +msgid "NEW COMMENT -- DOUBLE-CLICK TO EDIT" +msgstr "" + +#: constraint.cpp:952 +msgid "click center of comment text" +msgstr "" + +#: export.cpp:19 +msgid "" +"No solid model present; draw one with extrudes and revolves, or use Export 2d View to export bare " +"lines and curves." +msgstr "" + +#: export.cpp:61 +msgid "" +"Bad selection for export section. Please select:\n" +"\n" +" * nothing, with an active workplane (workplane is section plane)\n" +" * a face (section plane through face)\n" +" * a point and two line segments (plane through point and parallel to lines)\n" +msgstr "" + +#: export.cpp:818 +msgid "Active group mesh is empty; nothing to export." +msgstr "" + +#: exportvector.cpp:336 +msgid "freehand lines were replaced with continuous lines" +msgstr "" + +#: exportvector.cpp:338 +msgid "zigzag lines were replaced with continuous lines" +msgstr "" + +#: exportvector.cpp:592 +msgid "Some aspects of the drawing have no DXF equivalent and were not exported:\n" +msgstr "" + +#: exportvector.cpp:838 +msgid "PDF page size exceeds 200 by 200 inches; many viewers may reject this file." +msgstr "" + +#: file.cpp:44 group.cpp:91 +msgctxt "group-name" +msgid "sketch-in-plane" +msgstr "" + +#: file.cpp:62 +msgctxt "group-name" +msgid "#references" +msgstr "" + +#: file.cpp:555 +msgid "The file is empty. It may be corrupt." +msgstr "" + +#: file.cpp:560 +msgid "Unrecognized data in file. This file may be corrupt, or from a newer version of the program." +msgstr "" + +#: file.cpp:876 +msgctxt "title" +msgid "Missing File" +msgstr "" + +#: file.cpp:877 +#, c-format +msgctxt "dialog" +msgid "The linked file “%s” is not present." +msgstr "" + +#: file.cpp:879 +msgctxt "dialog" +msgid "" +"Do you want to locate it manually?\n" +"\n" +"If you decline, any geometry that depends on the missing file will be permanently removed." +msgstr "" + +#: file.cpp:882 +msgctxt "button" +msgid "&Yes" +msgstr "" + +#: file.cpp:884 +msgctxt "button" +msgid "&No" +msgstr "" + +#: file.cpp:886 solvespace.cpp:652 +msgctxt "button" +msgid "&Cancel" +msgstr "" + +#: graphicswin.cpp:41 +msgid "&File" +msgstr "" + +#: graphicswin.cpp:42 +msgid "&New" +msgstr "" + +#: graphicswin.cpp:43 +msgid "&Open..." +msgstr "" + +#: graphicswin.cpp:44 +msgid "Open &Recent" +msgstr "" + +#: graphicswin.cpp:45 +msgid "&Save" +msgstr "" + +#: graphicswin.cpp:46 +msgid "Save &As..." +msgstr "" + +#: graphicswin.cpp:48 +msgid "Export &Image..." +msgstr "" + +#: graphicswin.cpp:49 +msgid "Export 2d &View..." +msgstr "" + +#: graphicswin.cpp:50 +msgid "Export 2d &Section..." +msgstr "" + +#: graphicswin.cpp:51 +msgid "Export 3d &Wireframe..." +msgstr "" + +#: graphicswin.cpp:52 +msgid "Export Triangle &Mesh..." +msgstr "" + +#: graphicswin.cpp:53 +msgid "Export &Surfaces..." +msgstr "" + +#: graphicswin.cpp:54 +msgid "Im&port..." +msgstr "" + +#: graphicswin.cpp:57 +msgid "E&xit" +msgstr "" + +#: graphicswin.cpp:60 +msgid "&Edit" +msgstr "" + +#: graphicswin.cpp:61 +msgid "&Undo" +msgstr "" + +#: graphicswin.cpp:62 +msgid "&Redo" +msgstr "" + +#: graphicswin.cpp:63 +msgid "Re&generate All" +msgstr "" + +#: graphicswin.cpp:65 +msgid "Snap Selection to &Grid" +msgstr "" + +#: graphicswin.cpp:66 +msgid "Rotate Imported &90°" +msgstr "" + +#: graphicswin.cpp:68 +msgid "Cu&t" +msgstr "" + +#: graphicswin.cpp:69 +msgid "&Copy" +msgstr "" + +#: graphicswin.cpp:70 +msgid "&Paste" +msgstr "" + +#: graphicswin.cpp:71 +msgid "Paste &Transformed..." +msgstr "" + +#: graphicswin.cpp:72 +msgid "&Delete" +msgstr "" + +#: graphicswin.cpp:74 +msgid "Select &Edge Chain" +msgstr "" + +#: graphicswin.cpp:75 +msgid "Select &All" +msgstr "" + +#: graphicswin.cpp:76 +msgid "&Unselect All" +msgstr "" + +#: graphicswin.cpp:78 +msgid "&Line Styles..." +msgstr "" + +#: graphicswin.cpp:79 +msgid "&View Projection..." +msgstr "" + +#: graphicswin.cpp:81 +msgid "Con&figuration..." +msgstr "" + +#: graphicswin.cpp:84 +msgid "&View" +msgstr "" + +#: graphicswin.cpp:85 +msgid "Zoom &In" +msgstr "" + +#: graphicswin.cpp:86 +msgid "Zoom &Out" +msgstr "" + +#: graphicswin.cpp:87 +msgid "Zoom To &Fit" +msgstr "" + +#: graphicswin.cpp:89 +msgid "Align View to &Workplane" +msgstr "" + +#: graphicswin.cpp:90 +msgid "Nearest &Ortho View" +msgstr "" + +#: graphicswin.cpp:91 +msgid "Nearest &Isometric View" +msgstr "" + +#: graphicswin.cpp:92 +msgid "&Center View At Point" +msgstr "" + +#: graphicswin.cpp:94 +msgid "Show Snap &Grid" +msgstr "" + +#: graphicswin.cpp:95 +msgid "Darken Inactive Solids" +msgstr "" + +#: graphicswin.cpp:96 +msgid "Use &Perspective Projection" +msgstr "" + +#: graphicswin.cpp:97 +msgid "Show E&xploded View" +msgstr "" + +#: graphicswin.cpp:98 +msgid "Dimension &Units" +msgstr "" + +#: graphicswin.cpp:99 +msgid "Dimensions in &Millimeters" +msgstr "" + +#: graphicswin.cpp:100 +msgid "Dimensions in M&eters" +msgstr "" + +#: graphicswin.cpp:101 +msgid "Dimensions in &Inches" +msgstr "" + +#: graphicswin.cpp:102 +msgid "Dimensions in &Feet and Inches" +msgstr "" + +#: graphicswin.cpp:104 +msgid "Show &Toolbar" +msgstr "" + +#: graphicswin.cpp:105 +msgid "Show Property Bro&wser" +msgstr "" + +#: graphicswin.cpp:107 +msgid "&Full Screen" +msgstr "" + +#: graphicswin.cpp:109 +msgid "&New Group" +msgstr "" + +#: graphicswin.cpp:110 +msgid "Sketch In &3d" +msgstr "" + +#: graphicswin.cpp:111 +msgid "Sketch In New &Workplane" +msgstr "" + +#: graphicswin.cpp:113 +msgid "Step &Translating" +msgstr "" + +#: graphicswin.cpp:114 +msgid "Step &Rotating" +msgstr "" + +#: graphicswin.cpp:116 +msgid "E&xtrude" +msgstr "" + +#: graphicswin.cpp:117 +msgid "&Helix" +msgstr "" + +#: graphicswin.cpp:118 +msgid "&Lathe" +msgstr "" + +#: graphicswin.cpp:119 +msgid "Re&volve" +msgstr "" + +#: graphicswin.cpp:121 +msgid "Link / Assemble..." +msgstr "" + +#: graphicswin.cpp:122 +msgid "Link Recent" +msgstr "" + +#: graphicswin.cpp:124 +msgid "&Sketch" +msgstr "" + +#: graphicswin.cpp:125 +msgid "In &Workplane" +msgstr "" + +#: graphicswin.cpp:126 +msgid "Anywhere In &3d" +msgstr "" + +#: graphicswin.cpp:128 +msgid "Datum &Point" +msgstr "" + +#: graphicswin.cpp:129 +msgid "Wor&kplane" +msgstr "" + +#: graphicswin.cpp:131 +msgid "Line &Segment" +msgstr "" + +#: graphicswin.cpp:132 +msgid "C&onstruction Line Segment" +msgstr "" + +#: graphicswin.cpp:133 +msgid "&Rectangle" +msgstr "" + +#: graphicswin.cpp:134 +msgid "&Circle" +msgstr "" + +#: graphicswin.cpp:135 +msgid "&Arc of a Circle" +msgstr "" + +#: graphicswin.cpp:136 +msgid "&Bezier Cubic Spline" +msgstr "" + +#: graphicswin.cpp:138 +msgid "&Text in TrueType Font" +msgstr "" + +#: graphicswin.cpp:139 +msgid "I&mage" +msgstr "" + +#: graphicswin.cpp:141 +msgid "To&ggle Construction" +msgstr "" + +#: graphicswin.cpp:142 +msgid "Ta&ngent Arc at Point" +msgstr "" + +#: graphicswin.cpp:143 +msgid "Split Curves at &Intersection" +msgstr "" + +#: graphicswin.cpp:145 +msgid "&Constrain" +msgstr "" + +#: graphicswin.cpp:146 +msgid "&Distance / Diameter" +msgstr "" + +#: graphicswin.cpp:147 +msgid "Re&ference Dimension" +msgstr "" + +#: graphicswin.cpp:148 +msgid "A&ngle / Equal Angle" +msgstr "" + +#: graphicswin.cpp:149 +msgid "Reference An&gle" +msgstr "" + +#: graphicswin.cpp:150 +msgid "Other S&upplementary Angle" +msgstr "" + +#: graphicswin.cpp:151 +msgid "Toggle R&eference Dim" +msgstr "" + +#: graphicswin.cpp:153 +msgid "&Horizontal" +msgstr "" + +#: graphicswin.cpp:154 +msgid "&Vertical" +msgstr "" + +#: graphicswin.cpp:156 +msgid "&On Point / Curve / Plane" +msgstr "" + +#: graphicswin.cpp:157 +msgid "E&qual Length / Radius" +msgstr "" + +#: graphicswin.cpp:158 +msgid "Length / Arc Ra&tio" +msgstr "" + +#: graphicswin.cpp:159 +msgid "Length / Arc Diff&erence" +msgstr "" + +#: graphicswin.cpp:160 +msgid "At &Midpoint" +msgstr "" + +#: graphicswin.cpp:161 +msgid "S&ymmetric" +msgstr "" + +#: graphicswin.cpp:162 +msgid "Para&llel / Tangent" +msgstr "" + +#: graphicswin.cpp:163 +msgid "&Perpendicular" +msgstr "" + +#: graphicswin.cpp:164 +msgid "Same Orient&ation" +msgstr "" + +#: graphicswin.cpp:165 +msgid "Lock Point Where &Dragged" +msgstr "" + +#: graphicswin.cpp:167 +msgid "Comment" +msgstr "" + +#: graphicswin.cpp:169 +msgid "&Analyze" +msgstr "" + +#: graphicswin.cpp:170 +msgid "Measure &Volume" +msgstr "" + +#: graphicswin.cpp:171 +msgid "Measure A&rea" +msgstr "" + +#: graphicswin.cpp:172 +msgid "Measure &Perimeter" +msgstr "" + +#: graphicswin.cpp:173 +msgid "Show &Interfering Parts" +msgstr "" + +#: graphicswin.cpp:174 +msgid "Show &Naked Edges" +msgstr "" + +#: graphicswin.cpp:175 +msgid "Show &Center of Mass" +msgstr "" + +#: graphicswin.cpp:177 +msgid "Show &Underconstrained Points" +msgstr "" + +#: graphicswin.cpp:179 +msgid "&Trace Point" +msgstr "" + +#: graphicswin.cpp:180 +msgid "&Stop Tracing..." +msgstr "" + +#: graphicswin.cpp:181 +msgid "Step &Dimension..." +msgstr "" + +#: graphicswin.cpp:183 +msgid "&Help" +msgstr "" + +#: graphicswin.cpp:184 +msgid "&Language" +msgstr "" + +#: graphicswin.cpp:185 +msgid "&Website / Manual" +msgstr "" + +#: graphicswin.cpp:186 +msgid "&Go to GitHub commit" +msgstr "" + +#: graphicswin.cpp:188 +msgid "&About" +msgstr "" + +#: graphicswin.cpp:362 +msgid "(no recent files)" +msgstr "" + +#: graphicswin.cpp:370 +#, c-format +msgid "File '%s' does not exist." +msgstr "" + +#: graphicswin.cpp:779 +msgid "No workplane is active, so the grid will not appear." +msgstr "" + +#: graphicswin.cpp:794 +msgid "" +"The perspective factor is set to zero, so the view will always be a parallel projection.\n" +"\n" +"For a perspective projection, modify the perspective factor in the configuration screen. A value " +"around 0.3 is typical." +msgstr "" + +#: graphicswin.cpp:884 +msgid "Select a point; this point will become the center of the view on screen." +msgstr "" + +#: graphicswin.cpp:1193 +msgid "No additional entities share endpoints with the selected entities." +msgstr "" + +#: graphicswin.cpp:1211 +msgid "" +"To use this command, select a point or other entity from an linked part, or make a link group the " +"active group." +msgstr "" + +#: graphicswin.cpp:1234 +msgid "" +"No workplane is active. Activate a workplane (with Sketch -> In Workplane) to define the plane " +"for the snap grid." +msgstr "" + +#: graphicswin.cpp:1241 +msgid "" +"Can't snap these items to grid; select points, text comments, or constraints with a label. To " +"snap a line, select its endpoints." +msgstr "" + +#: graphicswin.cpp:1326 +msgid "No workplane selected. Activating default workplane for this group." +msgstr "" + +#: graphicswin.cpp:1329 +msgid "" +"No workplane is selected, and the active group does not have a default workplane. Try selecting a " +"workplane, or activating a sketch-in-new-workplane group." +msgstr "" + +#: graphicswin.cpp:1350 +msgid "" +"Bad selection for tangent arc at point. Select a single point, or select nothing to set up arc " +"parameters." +msgstr "" + +#: graphicswin.cpp:1361 +msgid "click point on arc (draws anti-clockwise)" +msgstr "" + +#: graphicswin.cpp:1362 +msgid "click to place datum point" +msgstr "" + +#: graphicswin.cpp:1363 +msgid "click first point of line segment" +msgstr "" + +#: graphicswin.cpp:1365 +msgid "click first point of construction line segment" +msgstr "" + +#: graphicswin.cpp:1366 +msgid "click first point of cubic segment" +msgstr "" + +#: graphicswin.cpp:1367 +msgid "click center of circle" +msgstr "" + +#: graphicswin.cpp:1368 +msgid "click origin of workplane" +msgstr "" + +#: graphicswin.cpp:1369 +msgid "click one corner of rectangle" +msgstr "" + +#: graphicswin.cpp:1370 +msgid "click top left of text" +msgstr "" + +#: graphicswin.cpp:1376 +msgid "click top left of image" +msgstr "" + +#: graphicswin.cpp:1402 +msgid "No entities are selected. Select entities before trying to toggle their construction state." +msgstr "" + +#: group.cpp:86 +msgctxt "group-name" +msgid "sketch-in-3d" +msgstr "" + +#: group.cpp:154 +msgid "" +"Bad selection for new sketch in workplane. This group can be created with:\n" +"\n" +" * a point (through the point, orthogonal to coordinate axes)\n" +" * a point and two line segments (through the point, parallel to the lines)\n" +" * a point and a normal (through the point, orthogonal to the normal)\n" +" * a workplane (copy of the workplane)\n" +msgstr "" + +#: group.cpp:170 +msgid "" +"Activate a workplane (Sketch -> In Workplane) before extruding. The sketch will be extruded " +"normal to the workplane." +msgstr "" + +#: group.cpp:179 +msgctxt "group-name" +msgid "extrude" +msgstr "" + +#: group.cpp:184 +msgid "Lathe operation can only be applied to planar sketches." +msgstr "" + +#: group.cpp:195 +msgid "" +"Bad selection for new lathe group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel to line / normal, " +"through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" + +#: group.cpp:205 +msgctxt "group-name" +msgid "lathe" +msgstr "" + +#: group.cpp:210 +msgid "Revolve operation can only be applied to planar sketches." +msgstr "" + +#: group.cpp:221 +msgid "" +"Bad selection for new revolve group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel to line / normal, " +"through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" + +#: group.cpp:233 +msgctxt "group-name" +msgid "revolve" +msgstr "" + +#: group.cpp:238 +msgid "Helix operation can only be applied to planar sketches." +msgstr "" + +#: group.cpp:249 +msgid "" +"Bad selection for new helix group. This group can be created with:\n" +"\n" +" * a point and a line segment or normal (revolved about an axis parallel to line / normal, " +"through point)\n" +" * a line segment (revolved about line segment)\n" +msgstr "" + +#: group.cpp:261 +msgctxt "group-name" +msgid "helix" +msgstr "" + +#: group.cpp:274 +msgid "" +"Bad selection for new rotation. This group can be created with:\n" +"\n" +" * a point, while locked in workplane (rotate in plane, about that point)\n" +" * a point and a line or a normal (rotate about an axis through the point, and parallel to " +"line / normal)\n" +msgstr "" + +#: group.cpp:287 +msgctxt "group-name" +msgid "rotate" +msgstr "" + +#: group.cpp:298 +msgctxt "group-name" +msgid "translate" +msgstr "" + +#: group.cpp:422 +msgid "(unnamed)" +msgstr "" + +#: groupmesh.cpp:710 +msgid "not closed contour, or not all same style!" +msgstr "" + +#: groupmesh.cpp:723 +msgid "points not all coplanar!" +msgstr "" + +#: groupmesh.cpp:725 +msgid "contour is self-intersecting!" +msgstr "" + +#: groupmesh.cpp:727 +msgid "zero-length edge!" +msgstr "" + +#: importmesh.cpp:136 +msgid "Text-formated STL files are not currently supported" +msgstr "" + +#: modify.cpp:252 +msgid "Must be sketching in workplane to create tangent arc." +msgstr "" + +#: modify.cpp:299 +msgid "" +"To create a tangent arc, select a point where two non-construction lines or circles in this group " +"and workplane join." +msgstr "" + +#: modify.cpp:386 +msgid "" +"Couldn't round this corner. Try a smaller radius, or try creating the desired geometry by hand " +"with tangency constraints." +msgstr "" + +#: modify.cpp:595 +msgid "Couldn't split this entity; lines, circles, or cubics only." +msgstr "" + +#: modify.cpp:622 +msgid "Must be sketching in workplane to split." +msgstr "" + +#: modify.cpp:629 +msgid "" +"Select two entities that intersect each other (e.g. two lines/circles/arcs or a line/circle/arc " +"and a point)." +msgstr "" + +#: modify.cpp:734 +msgid "Can't split; no intersection found." +msgstr "" + +#: mouse.cpp:558 +msgid "Assign to Style" +msgstr "" + +#: mouse.cpp:574 +msgid "No Style" +msgstr "" + +#: mouse.cpp:577 +msgid "Newly Created Custom Style..." +msgstr "" + +#: mouse.cpp:584 +msgid "Group Info" +msgstr "" + +#: mouse.cpp:604 +msgid "Style Info" +msgstr "" + +#: mouse.cpp:624 +msgid "Select Edge Chain" +msgstr "" + +#: mouse.cpp:630 +msgid "Toggle Reference Dimension" +msgstr "" + +#: mouse.cpp:636 +msgid "Other Supplementary Angle" +msgstr "" + +#: mouse.cpp:641 +msgid "Snap to Grid" +msgstr "" + +#: mouse.cpp:650 +msgid "Remove Spline Point" +msgstr "" + +#: mouse.cpp:685 +msgid "Add Spline Point" +msgstr "" + +#: mouse.cpp:689 +msgid "Cannot add spline point: maximum number of points reached." +msgstr "" + +#: mouse.cpp:714 +msgid "Toggle Construction" +msgstr "" + +#: mouse.cpp:730 +msgid "Delete Point-Coincident Constraint" +msgstr "" + +#: mouse.cpp:748 +msgid "Cut" +msgstr "" + +#: mouse.cpp:750 +msgid "Copy" +msgstr "" + +#: mouse.cpp:754 +msgid "Select All" +msgstr "" + +#: mouse.cpp:759 +msgid "Paste" +msgstr "" + +#: mouse.cpp:761 +msgid "Paste Transformed..." +msgstr "" + +#: mouse.cpp:766 +msgid "Delete" +msgstr "" + +#: mouse.cpp:769 +msgid "Unselect All" +msgstr "" + +#: mouse.cpp:776 +msgid "Unselect Hovered" +msgstr "" + +#: mouse.cpp:785 +msgid "Zoom to Fit" +msgstr "" + +#: mouse.cpp:987 mouse.cpp:1276 +msgid "click next point of line, or press Esc" +msgstr "" + +#: mouse.cpp:993 +msgid "Can't draw rectangle in 3d; first, activate a workplane with Sketch -> In Workplane." +msgstr "" + +#: mouse.cpp:1027 +msgid "click to place other corner of rectangle" +msgstr "" + +#: mouse.cpp:1048 +msgid "click to set radius" +msgstr "" + +#: mouse.cpp:1053 +msgid "Can't draw arc in 3d; first, activate a workplane with Sketch -> In Workplane." +msgstr "" + +#: mouse.cpp:1072 +msgid "click to place point" +msgstr "" + +#: mouse.cpp:1088 +msgid "click next point of cubic, or press Esc" +msgstr "" + +#: mouse.cpp:1093 +msgid "Sketching in a workplane already; sketch in 3d before creating new workplane." +msgstr "" + +#: mouse.cpp:1109 +msgid "Can't draw text in 3d; first, activate a workplane with Sketch -> In Workplane." +msgstr "" + +#: mouse.cpp:1126 +msgid "click to place bottom right of text" +msgstr "" + +#: mouse.cpp:1132 +msgid "Can't draw image in 3d; first, activate a workplane with Sketch -> In Workplane." +msgstr "" + +#: platform/gui.cpp:85 platform/gui.cpp:90 solvespace.cpp:583 +msgctxt "file-type" +msgid "SolveSpace models" +msgstr "" + +#: platform/gui.cpp:89 +msgctxt "file-type" +msgid "ALL" +msgstr "" + +#: platform/gui.cpp:91 +msgctxt "file-type" +msgid "IDF circuit board" +msgstr "" + +#: platform/gui.cpp:92 +msgctxt "file-type" +msgid "STL triangle mesh" +msgstr "" + +#: platform/gui.cpp:96 +msgctxt "file-type" +msgid "PNG image" +msgstr "" + +#: platform/gui.cpp:100 +msgctxt "file-type" +msgid "STL mesh" +msgstr "" + +#: platform/gui.cpp:101 +msgctxt "file-type" +msgid "Wavefront OBJ mesh" +msgstr "" + +#: platform/gui.cpp:102 +msgctxt "file-type" +msgid "Three.js-compatible mesh, with viewer" +msgstr "" + +#: platform/gui.cpp:103 +msgctxt "file-type" +msgid "Three.js-compatible mesh, mesh only" +msgstr "" + +#: platform/gui.cpp:104 +msgctxt "file-type" +msgid "VRML text file" +msgstr "" + +#: platform/gui.cpp:108 platform/gui.cpp:115 platform/gui.cpp:122 +msgctxt "file-type" +msgid "STEP file" +msgstr "" + +#: platform/gui.cpp:112 +msgctxt "file-type" +msgid "PDF file" +msgstr "" + +#: platform/gui.cpp:113 +msgctxt "file-type" +msgid "Encapsulated PostScript" +msgstr "" + +#: platform/gui.cpp:114 +msgctxt "file-type" +msgid "Scalable Vector Graphics" +msgstr "" + +#: platform/gui.cpp:116 platform/gui.cpp:123 +msgctxt "file-type" +msgid "DXF file (AutoCAD 2007)" +msgstr "" + +#: platform/gui.cpp:117 +msgctxt "file-type" +msgid "HPGL file" +msgstr "" + +#: platform/gui.cpp:118 +msgctxt "file-type" +msgid "G Code" +msgstr "" + +#: platform/gui.cpp:127 +msgctxt "file-type" +msgid "AutoCAD DXF and DWG files" +msgstr "" + +#: platform/gui.cpp:131 +msgctxt "file-type" +msgid "Comma-separated values" +msgstr "" + +#: platform/guigtk.cpp:1434 platform/guimac.mm:1513 platform/guiwin.cpp:1641 +msgid "untitled" +msgstr "" + +#: platform/guigtk.cpp:1445 platform/guigtk.cpp:1481 platform/guimac.mm:1471 +#: platform/guiwin.cpp:1639 +msgctxt "title" +msgid "Save File" +msgstr "" + +#: platform/guigtk.cpp:1446 platform/guigtk.cpp:1482 platform/guimac.mm:1454 +#: platform/guiwin.cpp:1645 +msgctxt "title" +msgid "Open File" +msgstr "" + +#: platform/guigtk.cpp:1449 platform/guigtk.cpp:1488 +msgctxt "button" +msgid "_Cancel" +msgstr "" + +#: platform/guigtk.cpp:1450 platform/guigtk.cpp:1486 +msgctxt "button" +msgid "_Save" +msgstr "" + +#: platform/guigtk.cpp:1451 platform/guigtk.cpp:1487 +msgctxt "button" +msgid "_Open" +msgstr "" + +#: solvespace.cpp:175 +msgctxt "title" +msgid "Autosave Available" +msgstr "" + +#: solvespace.cpp:176 +msgctxt "dialog" +msgid "An autosave file is available for this sketch." +msgstr "" + +#: solvespace.cpp:177 +msgctxt "dialog" +msgid "Do you want to load the autosave file instead?" +msgstr "" + +#: solvespace.cpp:178 +msgctxt "button" +msgid "&Load autosave" +msgstr "" + +#: solvespace.cpp:180 +msgctxt "button" +msgid "Do&n't Load" +msgstr "" + +#: solvespace.cpp:640 +msgctxt "title" +msgid "Modified File" +msgstr "" + +#: solvespace.cpp:642 +#, c-format +msgctxt "dialog" +msgid "Do you want to save the changes you made to the sketch “%s”?" +msgstr "" + +#: solvespace.cpp:645 +msgctxt "dialog" +msgid "Do you want to save the changes you made to the new sketch?" +msgstr "" + +#: solvespace.cpp:648 +msgctxt "dialog" +msgid "Your changes will be lost if you don't save them." +msgstr "" + +#: solvespace.cpp:649 +msgctxt "button" +msgid "&Save" +msgstr "" + +#: solvespace.cpp:651 +msgctxt "button" +msgid "Do&n't Save" +msgstr "" + +#: solvespace.cpp:672 +msgctxt "title" +msgid "(new sketch)" +msgstr "" + +#: solvespace.cpp:683 +msgctxt "title" +msgid "Property Browser" +msgstr "" + +#: solvespace.cpp:746 +msgid "" +"Constraints are currently shown, and will be exported in the toolpath. This is probably not what " +"you want; hide them by clicking the link at the top of the text window." +msgstr "" + +#: solvespace.cpp:834 +#, c-format +msgid "Can't identify file type from file extension of filename '%s'; try .dxf or .dwg." +msgstr "" + +#: solvespace.cpp:886 +msgid "Constraint must have a label, and must not be a reference dimension." +msgstr "" + +#: solvespace.cpp:890 +msgid "Bad selection for step dimension; select a constraint." +msgstr "" + +#: solvespace.cpp:914 +msgid "The assembly does not interfere, good." +msgstr "" + +#: solvespace.cpp:930 +#, c-format +msgid "" +"The volume of the solid model is:\n" +"\n" +" %s" +msgstr "" + +#: solvespace.cpp:939 +#, c-format +msgid "" +"\n" +"The volume of current group mesh is:\n" +"\n" +" %s" +msgstr "" + +#: solvespace.cpp:944 +msgid "" +"\n" +"\n" +"Curved surfaces have been approximated as triangles.\n" +"This introduces error, typically of around 1%." +msgstr "" + +#: solvespace.cpp:959 +#, c-format +msgid "" +"The surface area of the selected faces is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" + +#: solvespace.cpp:968 +msgid "" +"This group does not contain a correctly-formed 2d closed area. It is open, not coplanar, or self-" +"intersecting." +msgstr "" + +#: solvespace.cpp:980 +#, c-format +msgid "" +"The area of the region sketched in this group is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" + +#: solvespace.cpp:1000 +#, c-format +msgid "" +"The total length of the selected entities is:\n" +"\n" +" %s\n" +"\n" +"Curves have been approximated as piecewise linear.\n" +"This introduces error, typically of around 1%%." +msgstr "" + +#: solvespace.cpp:1006 +msgid "Bad selection for perimeter; select line segments, arcs, and curves." +msgstr "" + +#: solvespace.cpp:1022 +msgid "Bad selection for trace; select a single point." +msgstr "" + +#: solvespace.cpp:1049 +#, c-format +msgid "Couldn't write to '%s'" +msgstr "" + +#: solvespace.cpp:1079 +msgid "The mesh is self-intersecting (NOT okay, invalid)." +msgstr "" + +#: solvespace.cpp:1080 +msgid "The mesh is not self-intersecting (okay, valid)." +msgstr "" + +#: solvespace.cpp:1082 +msgid "The mesh has naked edges (NOT okay, invalid)." +msgstr "" + +#: solvespace.cpp:1083 +msgid "The mesh is watertight (okay, valid)." +msgstr "" + +#: solvespace.cpp:1086 +#, c-format +msgid "" +"\n" +"\n" +"The model contains %d triangles, from %d surfaces." +msgstr "" + +#: solvespace.cpp:1090 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"Zero problematic edges, good.%s" +msgstr "" + +#: solvespace.cpp:1093 +#, c-format +msgid "" +"%s\n" +"\n" +"%s\n" +"\n" +"%d problematic edges, bad.%s" +msgstr "" + +#: solvespace.cpp:1106 +#, c-format +msgid "" +"This is SolveSpace version %s.\n" +"\n" +"For more information, see http://solvespace.com/\n" +"\n" +"SolveSpace is free software: you are free to modify\n" +"and/or redistribute it under the terms of the GNU\n" +"General Public License (GPL) version 3 or later.\n" +"\n" +"There is NO WARRANTY, to the extent permitted by\n" +"law. For details, visit http://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues and other authors.\n" +msgstr "" + +#: style.cpp:185 +msgid "" +"Can't assign style to an entity that's derived from another entity; try assigning a style to this " +"entity's parent." +msgstr "" + +#: style.cpp:735 +msgid "Style name cannot be empty" +msgstr "" + +#: textscreens.cpp:837 +msgid "Can't repeat fewer than 1 time." +msgstr "" + +#: textscreens.cpp:841 +msgid "Can't repeat more than 999 times." +msgstr "" + +#: textscreens.cpp:866 +msgid "Group name cannot be empty" +msgstr "" + +#: textscreens.cpp:918 +msgid "Opacity must be between zero and one." +msgstr "" + +#: textscreens.cpp:953 +msgid "Radius cannot be zero or negative." +msgstr "" + +#: toolbar.cpp:18 +msgid "Sketch line segment" +msgstr "" + +#: toolbar.cpp:20 +msgid "Sketch rectangle" +msgstr "" + +#: toolbar.cpp:22 +msgid "Sketch circle" +msgstr "" + +#: toolbar.cpp:24 +msgid "Sketch arc of a circle" +msgstr "" + +#: toolbar.cpp:26 +msgid "Sketch curves from text in a TrueType font" +msgstr "" + +#: toolbar.cpp:28 +msgid "Sketch image from a file" +msgstr "" + +#: toolbar.cpp:30 +msgid "Create tangent arc at selected point" +msgstr "" + +#: toolbar.cpp:32 +msgid "Sketch cubic Bezier spline" +msgstr "" + +#: toolbar.cpp:34 +msgid "Sketch datum point" +msgstr "" + +#: toolbar.cpp:36 +msgid "Toggle construction" +msgstr "" + +#: toolbar.cpp:38 +msgid "Split lines / curves where they intersect" +msgstr "" + +#: toolbar.cpp:42 +msgid "Constrain distance / diameter / length" +msgstr "" + +#: toolbar.cpp:44 +msgid "Constrain angle" +msgstr "" + +#: toolbar.cpp:46 +msgid "Constrain to be horizontal" +msgstr "" + +#: toolbar.cpp:48 +msgid "Constrain to be vertical" +msgstr "" + +#: toolbar.cpp:50 +msgid "Constrain to be parallel or tangent" +msgstr "" + +#: toolbar.cpp:52 +msgid "Constrain to be perpendicular" +msgstr "" + +#: toolbar.cpp:54 +msgid "Constrain point on line / curve / plane / point" +msgstr "" + +#: toolbar.cpp:56 +msgid "Constrain symmetric" +msgstr "" + +#: toolbar.cpp:58 +msgid "Constrain equal length / radius / angle" +msgstr "" + +#: toolbar.cpp:60 +msgid "Constrain normals in same orientation" +msgstr "" + +#: toolbar.cpp:62 +msgid "Other supplementary angle" +msgstr "" + +#: toolbar.cpp:64 +msgid "Toggle reference dimension" +msgstr "" + +#: toolbar.cpp:68 +msgid "New group extruding active sketch" +msgstr "" + +#: toolbar.cpp:70 +msgid "New group rotating active sketch" +msgstr "" + +#: toolbar.cpp:72 +msgid "New group helix from active sketch" +msgstr "" + +#: toolbar.cpp:74 +msgid "New group revolve active sketch" +msgstr "" + +#: toolbar.cpp:76 +msgid "New group step and repeat rotating" +msgstr "" + +#: toolbar.cpp:78 +msgid "New group step and repeat translating" +msgstr "" + +#: toolbar.cpp:80 +msgid "New group in new workplane (thru given entities)" +msgstr "" + +#: toolbar.cpp:82 +msgid "New group in 3d" +msgstr "" + +#: toolbar.cpp:84 +msgid "New group linking / assembling file" +msgstr "" + +#: toolbar.cpp:88 +msgid "Nearest isometric view" +msgstr "" + +#: toolbar.cpp:90 +msgid "Align view to active workplane" +msgstr "" + +#: util.cpp:165 +msgctxt "title" +msgid "Error" +msgstr "" + +#: util.cpp:165 +msgctxt "title" +msgid "Message" +msgstr "" + +#: util.cpp:170 +msgctxt "button" +msgid "&OK" +msgstr "" + +#: view.cpp:127 +msgid "Scale cannot be zero or negative." +msgstr "" + +#: view.cpp:139 view.cpp:148 +msgid "Bad format: specify x, y, z" +msgstr "" diff --git a/res/qt-resources.qrc b/res/qt-resources.qrc new file mode 100644 index 0000000..6c470db --- /dev/null +++ b/res/qt-resources.qrc @@ -0,0 +1,6 @@ + + + + freedesktop/solvespace-48x48.png + + \ No newline at end of file diff --git a/res/shaders/edge.frag b/res/shaders/edge.frag new file mode 100644 index 0000000..2d2e8ac --- /dev/null +++ b/res/shaders/edge.frag @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +// Edge rendering shader +// +// Copyright 2016 Aleksey Egorov +//----------------------------------------------------------------------------- +const float feather = 0.5; + +uniform vec4 color; +uniform float pixel; +uniform float width; +uniform float patternLen; +uniform float patternScale; +uniform sampler2D pattern; + +varying vec3 fragLoc; + +void main() { + // lookup distance texture + vec4 v = texture2D(pattern, vec2(fragLoc.z / patternScale, 0.0)); + + // decode distance value + float val = dot(v, vec4(1.0, 1.0 / 255.0, 1.0 / 65025.0, 1.0 / 160581375.0)); + + // calculate cap + float dist = length(vec2(val * patternScale / (patternLen * width) + abs(fragLoc.x), fragLoc.y)); + + // perform antialiasing + float k = smoothstep(1.0 - 2.0 * feather * pixel / (width + feather * pixel), 1.0, abs(dist)); + + // perform alpha-test + if(k == 1.0) discard; + + // write resulting color + gl_FragColor = vec4(color.rgb, color.a * (1.0 - k)); +} diff --git a/res/shaders/edge.vert b/res/shaders/edge.vert new file mode 100644 index 0000000..3c5af3b --- /dev/null +++ b/res/shaders/edge.vert @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------- +// Edge rendering shader +// +// Copyright 2016 Aleksey Egorov +//----------------------------------------------------------------------------- +const float feather = 0.5; + +attribute vec3 pos; +attribute vec3 loc; +attribute vec3 tgt; + +uniform mat4 modelview; +uniform mat4 projection; +uniform float width; +uniform float pixel; + +varying vec3 fragLoc; + +void main() { + // get camera direction from modelview matrix + vec3 dir = vec3(modelview[0].z, modelview[1].z, modelview[2].z); + + // calculate line contour extension basis for constant width and caps + vec3 norm = normalize(cross(tgt, dir)); + norm = normalize(norm - dir * dot(dir, norm)); + vec3 perp = normalize(cross(dir, norm)); + + // calculate line extension width considering antialiasing + float ext = width + feather * pixel; + + // extend line contour + vec3 vertex = pos; + vertex += ext * loc.x * normalize(perp); + vertex += ext * loc.y * normalize(norm); + + // write fragment location for calculating caps and antialiasing + fragLoc = loc; + + // transform resulting vertex with modelview and projection matrices + gl_Position = projection * modelview * vec4(vertex, 1.0); +} diff --git a/res/shaders/imesh.frag b/res/shaders/imesh.frag new file mode 100644 index 0000000..1e7a74b --- /dev/null +++ b/res/shaders/imesh.frag @@ -0,0 +1,12 @@ +//----------------------------------------------------------------------------- +// Indexed Mesh rendering shader +// +// Copyright 2016 Aleksey Egorov +//----------------------------------------------------------------------------- +uniform vec4 color; +uniform sampler2D texture_; + +void main() { + if(texture2D(texture_, gl_FragCoord.xy / 32.0).a < 0.5) discard; + gl_FragColor = color; +} diff --git a/res/shaders/imesh.vert b/res/shaders/imesh.vert new file mode 100644 index 0000000..5e3e59a --- /dev/null +++ b/res/shaders/imesh.vert @@ -0,0 +1,13 @@ +//----------------------------------------------------------------------------- +// Indexed Mesh rendering shader +// +// Copyright 2016 Aleksey Egorov +//----------------------------------------------------------------------------- +attribute vec3 pos; + +uniform mat4 modelview; +uniform mat4 projection; + +void main() { + gl_Position = projection * modelview * vec4(pos, 1.0); +} diff --git a/res/shaders/imesh_point.frag b/res/shaders/imesh_point.frag new file mode 100644 index 0000000..db165e1 --- /dev/null +++ b/res/shaders/imesh_point.frag @@ -0,0 +1,15 @@ +//----------------------------------------------------------------------------- +// Point rendering shader +// +// Copyright 2016 Aleksey Egorov +//----------------------------------------------------------------------------- +const float feather = 0.5; + +uniform vec4 color; +uniform float pixel; +uniform float width; + +void main() { + // Rectangular points + gl_FragColor = color; +} diff --git a/res/shaders/imesh_point.vert b/res/shaders/imesh_point.vert new file mode 100644 index 0000000..07aff1b --- /dev/null +++ b/res/shaders/imesh_point.vert @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +// Point rendering shader +// +// Copyright 2016 Aleksey Egorov +//----------------------------------------------------------------------------- +const float feather = 0.5; + +attribute vec3 pos; +attribute vec2 loc; + +uniform mat4 modelview; +uniform mat4 projection; +uniform float width; +uniform float pixel; + +void main() { + // get camera vectors from modelview matrix + vec3 u = vec3(modelview[0].x, modelview[1].x, modelview[2].x); + vec3 v = vec3(modelview[0].y, modelview[1].y, modelview[2].y); + + // calculate point contour extension basis for constant width and caps + + // calculate point extension width considering antialiasing + float ext = width + feather * pixel; + + // extend point contour + vec3 vertex = pos; + vertex += ext * loc.x * normalize(u); + vertex += ext * loc.y * normalize(v); + + // transform resulting vertex with modelview and projection matrices + gl_Position = projection * modelview * vec4(vertex, 1.0); +} diff --git a/res/shaders/imesh_tex.frag b/res/shaders/imesh_tex.frag new file mode 100644 index 0000000..d3b3235 --- /dev/null +++ b/res/shaders/imesh_tex.frag @@ -0,0 +1,15 @@ +//----------------------------------------------------------------------------- +// Indexed Mesh rendering shader +// +// Copyright 2016 Aleksey Egorov +//----------------------------------------------------------------------------- +uniform vec4 color; +uniform sampler2D texture_; + +varying vec2 fragTex; + +void main() { + vec4 texColor = texture2D(texture_, fragTex); + if(texColor.a == 0.0) discard; + gl_FragColor = texColor * color; +} diff --git a/res/shaders/imesh_tex.vert b/res/shaders/imesh_tex.vert new file mode 100644 index 0000000..e048759 --- /dev/null +++ b/res/shaders/imesh_tex.vert @@ -0,0 +1,17 @@ +//----------------------------------------------------------------------------- +// Indexed Mesh rendering shader +// +// Copyright 2016 Aleksey Egorov +//----------------------------------------------------------------------------- +attribute vec3 pos; +attribute vec2 tex; + +uniform mat4 modelview; +uniform mat4 projection; + +varying vec2 fragTex; + +void main() { + fragTex = tex; + gl_Position = projection * modelview * vec4(pos, 1.0); +} diff --git a/res/shaders/imesh_texa.frag b/res/shaders/imesh_texa.frag new file mode 100644 index 0000000..89a124c --- /dev/null +++ b/res/shaders/imesh_texa.frag @@ -0,0 +1,13 @@ +//----------------------------------------------------------------------------- +// Indexed Mesh rendering shader +// +// Copyright 2016 Aleksey Egorov +//----------------------------------------------------------------------------- +uniform vec4 color; +uniform sampler2D texture_; + +varying vec2 fragTex; + +void main() { + gl_FragColor = vec4(color.rgb, color.a * texture2D(texture_, fragTex).TEX_ALPHA); +} diff --git a/res/shaders/mesh.frag b/res/shaders/mesh.frag new file mode 100644 index 0000000..8cb32c2 --- /dev/null +++ b/res/shaders/mesh.frag @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------------- +// Mesh rendering shader +// +// Copyright 2016 Aleksey Egorov +//----------------------------------------------------------------------------- +uniform vec3 lightDir0; +uniform vec3 lightDir1; +uniform float lightInt0; +uniform float lightInt1; +uniform float ambient; + +varying vec3 fragNormal; +varying vec4 fragColor; + +void main() { + vec3 result = fragColor.xyz * ambient; + vec3 normal = normalize(fragNormal); + + float light0 = clamp(dot(lightDir0, normal), 0.0, 1.0) * lightInt0 * (1.0 - ambient); + result += fragColor.rgb * light0; + + float light1 = clamp(dot(lightDir1, normal), 0.0, 1.0) * lightInt1 * (1.0 - ambient); + result += fragColor.rgb * light1; + + gl_FragColor = vec4(result, fragColor.a); +} diff --git a/res/shaders/mesh.vert b/res/shaders/mesh.vert new file mode 100644 index 0000000..c310ffc --- /dev/null +++ b/res/shaders/mesh.vert @@ -0,0 +1,21 @@ +//----------------------------------------------------------------------------- +// Mesh rendering shader +// +// Copyright 2016 Aleksey Egorov +//----------------------------------------------------------------------------- +attribute vec3 pos; +attribute vec3 nor; +attribute vec4 col; + +uniform mat4 modelview; +uniform mat4 projection; + +varying vec3 fragNormal; +varying vec4 fragColor; + +void main() { + fragNormal = vec3(modelview * vec4(nor, 0.0)); + fragColor = col; + + gl_Position = projection * modelview * vec4(pos, 1.0); +} diff --git a/res/shaders/mesh_fill.frag b/res/shaders/mesh_fill.frag new file mode 100644 index 0000000..c8b3d37 --- /dev/null +++ b/res/shaders/mesh_fill.frag @@ -0,0 +1,12 @@ +//----------------------------------------------------------------------------- +// Mesh rendering shader +// +// Copyright 2016 Aleksey Egorov +//----------------------------------------------------------------------------- +uniform vec4 color; +uniform sampler2D texture_; + +void main() { + if(texture2D(texture_, gl_FragCoord.xy / 32.0).TEX_ALPHA < 0.5) discard; + gl_FragColor = color; +} diff --git a/res/shaders/mesh_fill.vert b/res/shaders/mesh_fill.vert new file mode 100644 index 0000000..935efa6 --- /dev/null +++ b/res/shaders/mesh_fill.vert @@ -0,0 +1,13 @@ +//----------------------------------------------------------------------------- +// Mesh rendering shader +// +// Copyright 2016 Aleksey Egorov +//----------------------------------------------------------------------------- +attribute vec3 pos; + +uniform mat4 modelview; +uniform mat4 projection; + +void main() { + gl_Position = projection * modelview * vec4(pos, 1.0); +} diff --git a/res/shaders/outline.vert b/res/shaders/outline.vert new file mode 100644 index 0000000..7683a14 --- /dev/null +++ b/res/shaders/outline.vert @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------------- +// Outline rendering shader +// +// Copyright 2016 Aleksey Egorov +//----------------------------------------------------------------------------- +const int EMPHASIZED_AND_CONTOUR = 0; +const int EMPHASIZED_WITHOUT_CONTOUR = 1; +const int CONTOUR_ONLY = 2; + +const float feather = 0.5; + +attribute vec3 pos; +attribute vec4 loc; +attribute vec3 tgt; +attribute vec3 nol; +attribute vec3 nor; + +uniform mat4 modelview; +uniform mat4 projection; +uniform float width; +uniform float pixel; +uniform int mode; + +varying vec3 fragLoc; + +void main() { + // get camera direction from modelview matrix + vec3 dir = vec3(modelview[0].z, modelview[1].z, modelview[2].z); + + // perform outline visibility test + float ldot = dot(nol, dir); + float rdot = dot(nor, dir); + + bool isOutline = (ldot > -1e-6) == (rdot < 1e-6) || + (rdot > -1e-6) == (ldot < 1e-6); + bool isTagged = loc.w > 0.5; + + float visible = float((mode == CONTOUR_ONLY && isOutline) || + (mode == EMPHASIZED_AND_CONTOUR && (isOutline || isTagged)) || + (mode == EMPHASIZED_WITHOUT_CONTOUR && isTagged && !isOutline)); + + // calculate line contour extension basis for constant width and caps + vec3 norm = normalize(cross(tgt, dir)); + norm = normalize(norm - dir * dot(dir, norm)); + vec3 perp = normalize(cross(dir, norm)); + + // calculate line extension width considering antialiasing + float ext = (width + feather * pixel) * visible; + + // extend line contour + vec3 vertex = pos; + vertex += ext * loc.x * normalize(perp); + vertex += ext * loc.y * normalize(norm); + + // write fragment location for calculating caps and antialiasing + fragLoc = vec3(loc); + + // transform resulting vertex with modelview and projection matrices + gl_Position = projection * modelview * vec4(vertex, 1.0); +} diff --git a/res/threejs/SolveSpaceControls.js b/res/threejs/SolveSpaceControls.js new file mode 100644 index 0000000..ca74fd5 --- /dev/null +++ b/res/threejs/SolveSpaceControls.js @@ -0,0 +1,536 @@ +window.devicePixelRatio = window.devicePixelRatio || 1; + +SolvespaceCamera = function(renderWidth, renderHeight, scale, up, right, offset) { + THREE.Camera.call(this); + + this.type = 'SolvespaceCamera'; + + this.renderWidth = renderWidth; + this.renderHeight = renderHeight; + this.zoomScale = scale; /* Avoid namespace collision w/ THREE.Object.scale */ + this.up = up; + this.right = right; + this.offset = offset; + this.depthBias = 0; + + this.updateProjectionMatrix(); +}; + +SolvespaceCamera.prototype = Object.create(THREE.Camera.prototype); +SolvespaceCamera.prototype.constructor = SolvespaceCamera; +SolvespaceCamera.prototype.updateProjectionMatrix = function() { + var temp = new THREE.Matrix4(); + var offset = new THREE.Matrix4().makeTranslation(this.offset.x, this.offset.y, this.offset.z); + // Convert to right handed- do up cross right instead. + var n = new THREE.Vector3().crossVectors(this.up, this.right); + var rotate = new THREE.Matrix4().makeBasis(this.right, this.up, n); + rotate.transpose(); + /* Transpose of rotation matrix == inverse. Rotating the camera by a + basis is equivalent to rotating an object by the inverse of the + basis. To mimic Solvespace's behavior, we pan relative to the camera. + So we need to be rotated to where the camera is pointing before panning. + See: https://en.wikipedia.org/wiki/Change_of_basis#Two_dimensions */ + + /* TODO: If we want perspective, we need an additional matrix + here which will modify w for perspective divide. */ + var scale = new THREE.Matrix4().makeScale(2 * this.zoomScale / this.renderWidth, + 2 * this.zoomScale / this.renderHeight, this.zoomScale / 30000.0); + + temp.multiply(scale); + temp.multiply(rotate); + temp.multiply(offset); + + this.projectionMatrix.copy(temp); +}; + +SolvespaceCamera.prototype.NormalizeProjectionVectors = function() { + /* After rotating, up and right may no longer be orthogonal. + However, their cross product will produce the correct + rotated plane, and we can recover an orthogonal basis. */ + var n = new THREE.Vector3().crossVectors(this.right, this.up); + this.up = new THREE.Vector3().crossVectors(n, this.right); + this.right.normalize(); + this.up.normalize(); +}; + +SolvespaceCamera.prototype.rotate = function(right, up) { + var oldRight = new THREE.Vector3().copy(this.right).normalize(); + var oldUp = new THREE.Vector3().copy(this.up).normalize(); + this.up.applyAxisAngle(oldRight, up); + this.right.applyAxisAngle(oldUp, right); + this.NormalizeProjectionVectors(); +}; + +SolvespaceCamera.prototype.offsetProj = function(right, up) { + var shift = new THREE.Vector3(right * this.right.x + up * this.up.x, + right * this.right.y + up * this.up.y, + right * this.right.z + up * this.up.z); + this.offset.add(shift); +}; + +/* Calculate the offset in terms of up and right projection vectors +that will preserve the world coordinates of the current mouse position after +the zoom. */ +SolvespaceCamera.prototype.zoomTo = function(x, y, delta) { + // Get offset components in world coordinates, in terms of up/right. + var projOffsetX = this.offset.dot(this.right); + var projOffsetY = this.offset.dot(this.up); + + /* Remove offset before scaling so, that mouse position changes + proportionally to the model and independent of current offset. */ + var centerRightI = x/this.zoomScale - projOffsetX; + var centerUpI = y/this.zoomScale - projOffsetY; + var zoomFactor; + + /* Zoom 20% every 100 delta. */ + if(delta < 0) { + zoomFactor = (-delta * 0.002 + 1); + } + else if(delta > 0) { + zoomFactor = (delta * (-1.0/600.0) + 1); + } + else { + return; + } + + this.zoomScale = this.zoomScale * zoomFactor; + var centerRightF = x/this.zoomScale - projOffsetX; + var centerUpF = y/this.zoomScale - projOffsetY; + + this.offset.addScaledVector(this.right, centerRightF - centerRightI); + this.offset.addScaledVector(this.up, centerUpF - centerUpI); +}; + +SolvespaceControls = function(object, domElement) { + var _this = this; + this.object = object; + this.domElement = ( domElement !== undefined ) ? domElement : document; + + var threePan = new Hammer.Pan({event : 'threepan', pointers : 3, enable : false}); + var panAfterTap = new Hammer.Pan({event : 'panaftertap', enable : false}); + + this.touchControls = new Hammer.Manager(domElement, { + recognizers: [ + [Hammer.Pinch, { enable: true }], + [Hammer.Pan], + [Hammer.Tap], + ] + }); + + this.touchControls.add(threePan); + this.touchControls.add(panAfterTap); + + var changeEvent = { + type: 'change' + }; + var startEvent = { + type: 'start' + }; + var endEvent = { + type: 'end' + }; + + var _changed = false; + var _offsetPrev = new THREE.Vector2(0, 0); + var _offsetCur = new THREE.Vector2(0, 0); + var _rotatePrev = new THREE.Vector2(0, 0); + var _rotateCur = new THREE.Vector2(0, 0); + + // Used during touch events. + var _rotateOrig = new THREE.Vector2(0, 0); + var _offsetOrig = new THREE.Vector2(0, 0); + var _prevScale = 1.0; + + this.handleEvent = function(event) { + if (typeof this[event.type] == 'function') { + this[event.type](event); + } + } + + function mousedown(event) { + event.preventDefault(); + event.stopPropagation(); + + switch (event.button) { + case 0: + _rotateCur.set(event.screenX, + event.screenY); + _rotatePrev.copy(_rotateCur); + document.addEventListener('mousemove', mousemove_rotate, false); + document.addEventListener('mouseup', mouseup, false); + break; + case 2: + _offsetCur.set(event.screenX / window.devicePixelRatio, + event.screenY / window.devicePixelRatio); + _offsetPrev.copy(_offsetCur); + document.addEventListener('mousemove', mousemove_pan, false); + document.addEventListener('mouseup', mouseup, false); + break; + default: + break; + } + } + + function wheel( event ) { + event.preventDefault(); + /* FIXME: Width and height might not be supported universally, but + can be calculated? */ + var box = _this.domElement.getBoundingClientRect(); + object.zoomTo(event.clientX - box.width/2 - box.left, + -(event.clientY - box.height/2 - box.top), event.deltaY); + _changed = true; + } + + function mousemove_rotate(event) { + _rotateCur.set(event.screenX, + event.screenY); + var diff = new THREE.Vector2().subVectors(_rotateCur, _rotatePrev) + .multiplyScalar(1 / object.zoomScale); + object.rotate(-0.3 * Math.PI / 180 * diff.x * object.zoomScale, + -0.3 * Math.PI / 180 * diff.y * object.zoomScale); + _changed = true; + _rotatePrev.copy(_rotateCur); + } + + function mousemove_pan(event) { + _offsetCur.set(event.screenX / window.devicePixelRatio, + event.screenY / window.devicePixelRatio); + var diff = new THREE.Vector2().subVectors(_offsetCur, _offsetPrev) + .multiplyScalar(window.devicePixelRatio / object.zoomScale); + object.offsetProj(diff.x, -diff.y); + _changed = true; + _offsetPrev.copy(_offsetCur); + } + + function mouseup(event) { + /* TODO: Opera mouse gestures will intercept this event, making it + possible to have multiple mousedown events consecutively without + a corresponding mouseup (so multiple viewports can be rotated/panned + simultaneously). Disable mouse gestures for now. */ + event.preventDefault(); + event.stopPropagation(); + + switch (event.button) { + case 0: + document.removeEventListener('mousemove', mousemove_rotate); + document.removeEventListener('mouseup', mouseup); + break; + case 2: + document.removeEventListener('mousemove', mousemove_pan); + document.removeEventListener('mouseup', mouseup); + break; + } + + _this.dispatchEvent(endEvent); + } + + function pan(event) { + /* neWcur - prev does not necessarily equal (cur + diff) - prev. + Floating point is not associative. */ + var touchDiff = new THREE.Vector2(event.deltaX, event.deltaY); + _rotateCur.addVectors(_rotateOrig, touchDiff); + var incDiff = new THREE.Vector2().subVectors(_rotateCur, _rotatePrev) + .multiplyScalar(1 / object.zoomScale); + object.rotate(-0.3 * Math.PI / 180 * incDiff.x * object.zoomScale, + -0.3 * Math.PI / 180 * incDiff.y * object.zoomScale); + _changed = true; + _rotatePrev.copy(_rotateCur); + } + + function panstart(event) { + /* TODO: Dynamically enable pan function? */ + _rotateOrig.copy(_rotateCur); + } + + function pinchstart(event) { + _prevScale = event.scale; + } + + function pinch(event) { + /* FIXME: Width and height might not be supported universally, but + can be calculated? */ + var box = _this.domElement.getBoundingClientRect(); + + /* 16.6... pixels chosen heuristically... matches my touchpad. */ + if (event.scale < _prevScale) { + object.zoomTo(event.center.x - box.width/2 - box.left, + -(event.center.y - box.height/2 - box.top), 100/6.0); + _changed = true; + } else if (event.scale > _prevScale) { + object.zoomTo(event.center.x - box.width/2 - box.left, + -(event.center.y - box.height/2 - box.top), -100/6.0); + _changed = true; + } + + _prevScale = event.scale; + } + + /* A tap will enable panning/disable rotate. */ + function tap(event) { + panAfterTap.set({enable : true}); + _this.touchControls.get('pan').set({enable : false}); + } + + function panaftertap(event) { + var touchDiff = new THREE.Vector2(event.deltaX, event.deltaY); + _offsetCur.addVectors(_offsetOrig, touchDiff); + var incDiff = new THREE.Vector2().subVectors(_offsetCur, _offsetPrev) + .multiplyScalar(1 / object.zoomScale); + object.offsetProj(incDiff.x, -incDiff.y); + _changed = true; + _offsetPrev.copy(_offsetCur); + } + + function panaftertapstart(event) { + _offsetOrig.copy(_offsetCur); + } + + function panaftertapend(event) { + panAfterTap.set({enable : false}); + _this.touchControls.get('pan').set({enable : true}); + } + + function contextmenu(event) { + event.preventDefault(); + } + + this.update = function() { + if (_changed) { + _this.dispatchEvent(changeEvent); + _changed = false; + } + }; + + this.domElement.addEventListener('mousedown', mousedown, false); + this.domElement.addEventListener('wheel', wheel, false); + this.domElement.addEventListener('contextmenu', contextmenu, false); + + /* Hammer.on wraps addEventListener */ + // Rotate + this.touchControls.on('pan', pan); + this.touchControls.on('panstart', panstart); + + // Zoom + this.touchControls.on('pinch', pinch); + this.touchControls.on('pinchstart', pinchstart); + + //Pan + this.touchControls.on('tap', tap); + this.touchControls.on('panaftertapstart', panaftertapstart); + this.touchControls.on('panaftertap', panaftertap); + this.touchControls.on('panaftertapend', panaftertapend); +}; + +SolvespaceControls.prototype = Object.create(THREE.EventDispatcher.prototype); +SolvespaceControls.prototype.constructor = SolvespaceControls; + + +solvespace = function(obj, params) { + var scene, edgeScene, camera, edgeCamera, renderer; + var geometry, controls, material, mesh, edges; + var width, height, scale, offset, projRight, projUp; + var directionalLightArray = []; + var inheritedWidth = false, inheritedHeight = false; + + if (typeof params === "undefined" || !("width" in params)) { + width = window.innerWidth; + inheritedWidth = true; + } else { + width = params.width; + } + + if (typeof params === "undefined" || !("height" in params)) { + height = window.innerHeight; + inheritedHeight = true; + } else { + height = params.height; + } + + if (typeof params === "undefined" || !("scale" in params)) { + scale = 5; + } else { + scale = params.scale; + } + + if (typeof params === "undefined" || !("offset" in params)) { + offset = new THREE.Vector3(0, 0, 0); + } else { + offset = params.offset; + } + + if (typeof params === "undefined" || !("projUp" in params)) { + projUp = new THREE.Vector3(0, 1, -1); + } else { + projUp = params.projUp; + } + + if (typeof params === "undefined" || !("projRight" in params)) { + projRight = new THREE.Vector3(1, 0, -1); + } else { + projRight = params.projRight; + } + + var domElement = init(); + lightUpdate(); + render(); + return domElement; + + function init() { + scene = new THREE.Scene(); + edgeScene = new THREE.Scene(); + + camera = new SolvespaceCamera(width, height, scale, projUp, projRight, offset); + camera.NormalizeProjectionVectors(); + + mesh = createMesh(obj); + scene.add(mesh); + edges = createEdges(obj); + edgeScene.add(edges); + + for (var i = 0; i < obj.lights.d.length; i++) { + var lightColor = new THREE.Color(obj.lights.d[i].intensity, + obj.lights.d[i].intensity, obj.lights.d[i].intensity); + var directionalLight = new THREE.DirectionalLight(lightColor, 1); + directionalLight.position.set(obj.lights.d[i].direction[0], + obj.lights.d[i].direction[1], obj.lights.d[i].direction[2]); + directionalLightArray.push(directionalLight); + scene.add(directionalLight); + } + + var lightColor = new THREE.Color(obj.lights.a, obj.lights.a, obj.lights.a); + var ambientLight = new THREE.AmbientLight(lightColor.getHex()); + scene.add(ambientLight); + + renderer = new THREE.WebGLRenderer({ antialias: true}); + renderer.setSize(width * window.devicePixelRatio, height * window.devicePixelRatio); + renderer.autoClear = false; + renderer.domElement.style = + "width: " + width + "px;" + + "height: " + height + "px;"; + + controls = new SolvespaceControls(camera, renderer.domElement); + controls.addEventListener("change", render); + controls.addEventListener("change", lightUpdate); + + if(inheritedWidth || inheritedHeight) { + window.addEventListener("resize", resize); + } + + animate(); + return renderer.domElement; + } + + function resize() { + scale = camera.zoomScale; + if(inheritedWidth) { + scale *= window.innerWidth / width; + width = window.innerWidth; + } + if(inheritedHeight) { + scale *= window.innerHeight / height; + height = window.innerHeight; + } + + camera.renderWidth = width; + camera.renderHeight = height; + camera.zoomScale = scale; + + renderer.setSize(width * window.devicePixelRatio, height * window.devicePixelRatio); + renderer.domElement.style = + "width: " + width + "px;" + + "height: " + height + "px;"; + + render(); + } + + function animate() { + requestAnimationFrame(animate); + controls.update(); + } + + function render() { + var context = renderer.getContext(); + camera.updateProjectionMatrix(); + renderer.clear(); + + context.depthRange(0.1, 1); + renderer.render(scene, camera); + + context.depthRange(0.1-(2/60000.0), 1-(2/60000.0)); + renderer.render(edgeScene, camera); + } + + function lightUpdate() { + var changeBasis = new THREE.Matrix4(); + + // The original light positions were in camera space. + // Project them into standard space using camera's basis + // vectors (up, target, and their cross product). + var n = new THREE.Vector3().crossVectors(camera.up, camera.right); + changeBasis.makeBasis(camera.right, camera.up, n); + + for (var i = 0; i < 2; i++) { + var newLightPos = new THREE.Vector3(obj.lights.d[i].direction[0], + obj.lights.d[i].direction[1], + obj.lights.d[i].direction[2]).applyMatrix4(changeBasis); + directionalLightArray[i].position.set(newLightPos[0], + newLightPos[1], newLightPos[2]); + } + } + + function createMesh(meshObj) { + var geometry = new THREE.Geometry(); + var materialIndex = 0; + var materialList = []; + var opacitiesSeen = {}; + + for (var i = 0; i < meshObj.points.length; i++) { + geometry.vertices.push(new THREE.Vector3(meshObj.points[i][0], + meshObj.points[i][1], meshObj.points[i][2])); + } + + for (var i = 0; i < meshObj.faces.length; i++) { + var currOpacity = ((meshObj.colors[i] & 0xFF000000) >>> 24) / 255.0; + if (opacitiesSeen[currOpacity] === undefined) { + opacitiesSeen[currOpacity] = materialIndex; + materialIndex++; + materialList.push(new THREE.MeshLambertMaterial({ + vertexColors: THREE.FaceColors, + opacity: currOpacity, + transparent: true, + side: THREE.DoubleSide + })); + } + + geometry.faces.push(new THREE.Face3(meshObj.faces[i][0], + meshObj.faces[i][1], meshObj.faces[i][2], + [new THREE.Vector3(meshObj.normals[i][0][0], + meshObj.normals[i][0][1], meshObj.normals[i][0][2]), + new THREE.Vector3(meshObj.normals[i][1][0], + meshObj.normals[i][1][1], meshObj.normals[i][1][2]), + new THREE.Vector3(meshObj.normals[i][2][0], + meshObj.normals[i][2][1], meshObj.normals[i][2][2])], + new THREE.Color(meshObj.colors[i] & 0x00FFFFFF), + opacitiesSeen[currOpacity])); + } + + geometry.computeBoundingSphere(); + return new THREE.Mesh(geometry, materialList); + } + + function createEdges(meshObj) { + var geometry = new THREE.Geometry(); + var material = new THREE.LineBasicMaterial(); + + for (var i = 0; i < meshObj.edges.length; i++) { + geometry.vertices.push(new THREE.Vector3(meshObj.edges[i][0][0], + meshObj.edges[i][0][1], meshObj.edges[i][0][2]), + new THREE.Vector3(meshObj.edges[i][1][0], + meshObj.edges[i][1][1], meshObj.edges[i][1][2])); + } + + geometry.computeBoundingSphere(); + return new THREE.LineSegments(geometry, material); + } +}; + diff --git a/res/threejs/hammer-2.0.8.js.gz b/res/threejs/hammer-2.0.8.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..764a580fa9dc21548397ffa5189cfc43a462394d GIT binary patch literal 17503 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 literal 0 HcmV?d00001 diff --git a/res/win32/manifest.xml b/res/win32/manifest.xml new file mode 100644 index 0000000..7c49a2e --- /dev/null +++ b/res/win32/manifest.xml @@ -0,0 +1,28 @@ + + + +Parametric 3d CAD tool. + + + PerMonitorV2, PerMonitor + true + + + + + + + + diff --git a/res/win32/versioninfo.rc.in b/res/win32/versioninfo.rc.in new file mode 100644 index 0000000..1a3179f --- /dev/null +++ b/res/win32/versioninfo.rc.in @@ -0,0 +1,29 @@ +1 VERSIONINFO +FILEVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},0,0 +PRODUCTVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},0,0 +FILEFLAGSMASK 0 +FILEFLAGS 0 +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_APP +FILESUBTYPE 0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "04090000" + BEGIN + VALUE "CompanyName", "The SolveSpace authors" + VALUE "ProductName", "SolveSpace" + VALUE "ProductVersion", "${PROJECT_VERSION}~${solvespace_GIT_HASH}" + VALUE "FileDescription", "SolveSpace, a parametric 2d/3d CAD" + VALUE "FileVersion", "${PROJECT_VERSION}~${solvespace_GIT_HASH}" + VALUE "OriginalFilename", "solvespace.exe" + VALUE "InternalName", "solvespace" + VALUE "LegalCopyright", "(c) 2008-2026 Jonathan Westhues and other authors" + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 0 + END +END diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..3af2075 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,526 @@ +include(GNUInstallDirs) + +# configuration + +set(HAVE_SPACEWARE ${SPACEWARE_FOUND}) + +if(NOT WIN32 OR APPLE) + if(GTKMM_gtkmm-3.0_VERSION VERSION_LESS "3.24.0") + set(HAVE_GTK_FILECHOOSERNATIVE 0) + else() + set(HAVE_GTK_FILECHOOSERNATIVE 1) + endif() +endif() + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.in + ${CMAKE_CURRENT_BINARY_DIR}/config.h) + +if(USE_QT_GUI) + set(CMAKE_AUTOMOC ON) + find_package(Qt6 COMPONENTS Core Gui OpenGLWidgets Widgets REQUIRED) + # find_package(Qt5 COMPONENTS Core Gui Widgets REQUIRED) + qt_add_resources(RESOURCE_FILES ../res/qt-resources.qrc) +endif() + +# solvespace dependencies +add_library(slvs_deps INTERFACE) +target_include_directories(slvs_deps SYSTEM INTERFACE + ${OPENGL_INCLUDE_DIR} + ${ZLIB_INCLUDE_DIR} + ${PNG_PNG_INCLUDE_DIR} + ${FREETYPE_INCLUDE_DIRS} + ${MIMALLOC_INCLUDE_DIR} + ${EIGEN3_INCLUDE_DIRS}) +target_link_libraries(slvs_deps INTERFACE + dxfrw + ${ZLIB_LIBRARY} + ${PNG_LIBRARY} + ${FREETYPE_LIBRARY} + mimalloc-static) + +if(NOT USE_QT_GUI OR ENABLE_CLI) + target_include_directories(slvs_deps SYSTEM INTERFACE ${CAIRO_INCLUDE_DIRS}) + target_link_libraries(slvs_deps INTERFACE ${CAIRO_LIBRARIES}) +endif() + +if(Backtrace_FOUND) + target_include_directories(slvs_deps SYSTEM INTERFACE + ${Backtrace_INCLUDE_DIRS}) + target_link_libraries(slvs_deps INTERFACE + ${Backtrace_LIBRARY}) +endif() + +if(SPACEWARE_FOUND) + target_include_directories(slvs_deps SYSTEM INTERFACE + ${SPACEWARE_INCLUDE_DIR}) + target_link_libraries(slvs_deps INTERFACE + ${SPACEWARE_LIBRARIES}) +endif() + +if(ENABLE_OPENMP) + target_link_libraries(slvs_deps INTERFACE slvs_openmp) +endif() + +target_compile_options(slvs_deps + INTERFACE ${COVERAGE_FLAGS}) + +# platform utilities +if(APPLE) + target_link_libraries(slvs_deps INTERFACE + ${APPKIT_LIBRARY}) +endif() + +# Solver +add_library(slvs-solver INTERFACE) +target_sources(slvs-solver INTERFACE + dsc.h + expr.h + handle.h + param.h + platform/platform.h + solvespace.h + + constrainteq.cpp + entity.cpp + expr.cpp + platform/platformbase.cpp + system.cpp + util.cpp +) +target_include_directories(slvs-solver SYSTEM INTERFACE + ${MIMALLOC_INCLUDE_DIR} + ${EIGEN3_INCLUDE_DIRS}) +target_include_directories(slvs-solver INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}) + +# Needed for CI +file(WRITE ${CMAKE_BINARY_DIR}/version.env "\ +VERSION_MAJOR=${PROJECT_VERSION_MAJOR}\n\ +VERSION_MINOR=${PROJECT_VERSION_MINOR}\n\ +VERSION_PATCH=0\n\ +VERSION_GIT_HASH=${solvespace_GIT_HASH}\n\ +") + +if(ENABLE_GUI OR ENABLE_CLI) + set(every_platform_SOURCES + platform/guiwin.cpp + platform/guigtk.cpp + platform/guimac.mm + platform/guihtml.cpp) + + # solvespace library + + set(solvespace_core_gl_SOURCES + solvespace.cpp) + + add_library(solvespace-core STATIC + polygon.h + sketch.h + ui.h + render/render.h + srf/surface.h + + bsp.cpp + clipboard.cpp + confscreen.cpp + constraint.cpp + describescreen.cpp + draw.cpp + drawconstraint.cpp + drawentity.cpp + export.cpp + exportstep.cpp + exportvector.cpp + file.cpp + generate.cpp + graphicswin.cpp + group.cpp + groupmesh.cpp + importdxf.cpp + importidf.cpp + importmesh.cpp + mesh.cpp + modify.cpp + mouse.cpp + polyline.cpp + polygon.cpp + resource.cpp + request.cpp + style.cpp + textscreens.cpp + textwin.cpp + toolbar.cpp + ttf.cpp + undoredo.cpp + view.cpp + platform/platform.cpp + platform/gui.cpp + render/render.cpp + render/render2d.cpp + srf/boolean.cpp + srf/curve.cpp + srf/merge.cpp + srf/ratpoly.cpp + srf/raycast.cpp + srf/shell.cpp + srf/surface.cpp + srf/surfinter.cpp + srf/triangulate.cpp) + + target_include_directories(solvespace-core PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR}) + target_link_libraries(solvespace-core PRIVATE slvs-solver) + target_link_libraries(solvespace-core PUBLIC slvs_deps) + + # solvespace translations + + if(HAVE_GETTEXT) + get_target_property(solvespace_core_SOURCES solvespace-core SOURCES) + set(inputs + ${solvespace_core_SOURCES} + ${every_platform_SOURCES} + ${solvespace_core_gl_SOURCES}) + + set(templ_po ${CMAKE_CURRENT_BINARY_DIR}/../res/messages.po) + + set(output_pot ${CMAKE_CURRENT_SOURCE_DIR}/../res/messages.pot) + set(output_po ${CMAKE_CURRENT_SOURCE_DIR}/../res/locales/en_US.po) + file(GLOB locale_pos ${CMAKE_CURRENT_SOURCE_DIR}/../res/locales/*.po) + + string(REPLACE ${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR} + gen_output_pot ${output_pot}.gen) + string(REPLACE ${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR} + gen_output_po ${output_po}.gen) + foreach(locale_po ${locale_pos}) + string(REPLACE ${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR} + gen_locale_po ${locale_po}.gen) + list(APPEND gen_locale_pos ${gen_locale_po}) + endforeach() + + add_custom_command( + OUTPUT ${gen_output_pot} + COMMAND ${XGETTEXT} + --language=C++ + --keyword --keyword=_ --keyword=N_ --keyword=C_:2,1c --keyword=CN_:2,1c + --force-po --width=100 --sort-by-file + --package-name=SolveSpace + --package-version=${PROJECT_VERSION} + "--copyright-holder=the PACKAGE authors" + --msgid-bugs-address=phkahler@gmail.com + --from-code=utf-8 --output=${gen_output_pot} ${inputs} + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${gen_output_pot} ${output_pot} + DEPENDS ${inputs} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Extracting translations" + VERBATIM) + + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/../res/locales) + + # en_US is a bit special; we pre-fill the msgstrs from msgids, instead of (as would normally + # happen) leaving them empty. + add_custom_command( + OUTPUT ${gen_output_po} + COMMAND ${MSGINIT} + --locale=en_US --no-translator + --output=${templ_po} --input=${gen_output_pot} + COMMAND ${MSGMERGE} + --force-po --no-fuzzy-matching + --output=${gen_output_po} ${output_po} ${templ_po} + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${gen_output_po} ${output_po} + DEPENDS ${gen_output_pot} + COMMENT "Updating en_US translations" + VERBATIM) + + foreach(locale_po ${locale_pos}) + string(REPLACE ${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR} + gen_locale_po ${locale_po}.gen) + + get_filename_component(locale_name ${locale_po} NAME_WE) + if(locale_name STREQUAL "en_US") + continue() + endif() + + add_custom_command( + OUTPUT ${gen_locale_po} + COMMAND ${MSGMERGE} + --no-fuzzy-matching + --output=${gen_locale_po} ${locale_po} ${gen_output_pot} + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${gen_locale_po} ${locale_po} + DEPENDS ${gen_output_pot} + COMMENT "Updating ${locale_name} translations" + VERBATIM) + endforeach() + + add_custom_target(translate_solvespace + DEPENDS ${gen_output_pot} ${gen_output_po} ${gen_locale_pos}) + endif() +endif() + +# solvespace graphical executable + +if(ENABLE_GUI) + add_executable(solvespace WIN32 MACOSX_BUNDLE + ${solvespace_core_gl_SOURCES} + $) + + if(NOT USE_QT_GUI) + target_sources(solvespace PRIVATE platform/entrygui.cpp) + endif() + + add_dependencies(solvespace + resources) + + target_link_libraries(solvespace + PRIVATE + solvespace-core + ${OPENGL_LIBRARIES}) + + # OpenGL version + if(OPENGL STREQUAL 3) + target_sources(solvespace PRIVATE + render/gl3shader.cpp + render/rendergl3.cpp) + elseif(OPENGL STREQUAL 1) + target_sources(solvespace PRIVATE + render/rendergl1.cpp) + else() + message(FATAL_ERROR "Unsupported OpenGL version ${OPENGL}") + endif() + + # Platform-specific + if(WIN32 AND NOT USE_QT_GUI) + target_sources(solvespace PRIVATE + platform/guiwin.cpp) + + target_link_libraries(solvespace PRIVATE comctl32) + elseif(APPLE) + target_compile_options(solvespace PRIVATE -fobjc-arc) + target_compile_definitions(solvespace PRIVATE GL_SILENCE_DEPRECATION) + + target_sources(solvespace PRIVATE + platform/guimac.mm) + set_target_properties(solvespace PROPERTIES + OUTPUT_NAME SolveSpace + XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME "YES" + XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.solvespace" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + elseif(EMSCRIPTEN) + # Due to the response file hack below, we need a version of Emscripten that + # passes arguments via a response file to the `file_packager` utility, but + # this was not the case prior to version 4.0.8 of Emscripten. + # See https://github.com/emscripten-core/emscripten/pull/24107 + if(NOT EMSCRIPTEN_VERSION OR EMSCRIPTEN_VERSION VERSION_LESS "4.0.8") + message(FATAL_ERROR "The Emscripten build requires at least version 4.0.8") + endif() + + target_compile_features(solvespace PRIVATE cxx_std_17) + + set(SHELL "${CMAKE_CURRENT_SOURCE_DIR}/platform/html/emshell.html") + # Add internal quotes to paths used in the response file to ensure that paths + # that contain spaces are handled correctly + set(RESP_LINK_FLAGS + --bind --shell-file "\"${SHELL}\"" --no-heap-copy + "-s ALLOW_MEMORY_GROWTH=1" "-s WASM=1" + "-s ASYNCIFY=1" "-s DYNCALLS=1" "-s ASSERTIONS=1" + "-s TOTAL_STACK=33554432" "-s TOTAL_MEMORY=134217728") + + get_target_property(resource_list resources SOURCES) + foreach(resource ${resource_list}) + # Strip everything before `res/...`, and map the source path to the stripped + # path in the virtual Emscripten fileystem + file(RELATIVE_PATH target_path "${CMAKE_SOURCE_DIR}" "${resource}") + list(APPEND RESP_LINK_FLAGS "--preload-file \"${resource}\"@\"${target_path}\"") + endforeach() + + if(CMAKE_BUILD_TYPE STREQUAL Debug) + list(APPEND RESP_LINK_FLAGS + --emrun --emit-symbol-map + "-s DEMANGLE_SUPPORT=1" + "-s SAFE_HEAP=1") + endif() + + target_sources(solvespace PRIVATE + platform/guihtml.cpp) + + # Pass arguments through a response file in order to avoid exceeding the maximum + # command line length (the Emscripten tools on Windows are all wrapped by batch + # scripts, and as such are subject to the CMD length limit of 8KiB). + set(resp_file_path "${CMAKE_CURRENT_BINARY_DIR}/emscripten_flags.rsp") + file(GENERATE OUTPUT "${resp_file_path}" CONTENT "$") + + # NOTE: it would be ideal if we could portably specify that linking depends on the + # response file (and the files that are specified in it), but unfortunately there + # is no way to reliably trigger a relink like that without using LINK_DEPENDS, + # which only works with the Ninja and Makefile generators. + # This means that using e.g. the Visual Studio of Xcode generators will not work + # correctly when any of the link time dependencies changes, so the output artefact + # needs to be explicitly deleted in order to trigger a relink. + set_target_properties(solvespace PROPERTIES + SUFFIX ".html" + LINK_FLAGS "@${resp_file_path}" + LINK_DEPENDS "${SHELL};${resp_file_path};${resource_list}") + + add_custom_command( + TARGET solvespace POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/platform/html/solvespaceui.css + ${EXECUTABLE_OUTPUT_PATH}/solvespaceui.css + COMMENT "Copying UI stylesheet" + VERBATIM) + add_custom_command( + TARGET solvespace POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/platform/html/solvespaceui.js + ${EXECUTABLE_OUTPUT_PATH}/solvespaceui.js + COMMENT "Copying UI script solvespaceui.js" + VERBATIM) + add_custom_command( + TARGET solvespace POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/platform/html/filemanagerui.js + ${EXECUTABLE_OUTPUT_PATH}/filemanagerui.js + COMMENT "Copying UI script filemanagerui.sj" + VERBATIM) + elseif(USE_QT_GUI) + set_target_properties(solvespace PROPERTIES + OUTPUT_NAME solvespace-qt) + + target_sources(solvespace PRIVATE + platform/guiqt.cpp + platform/guiqt.h + ${RESOURCE_FILES}) + + target_include_directories(solvespace SYSTEM PRIVATE + ${FONTCONFIG_INCLUDE_DIRS}) + target_link_directories(solvespace PRIVATE + ${FONTCONFIG_LIBRARY_DIRS}) + target_link_libraries(solvespace PRIVATE + ${FONTCONFIG_LIBRARIES} + Qt6::Core Qt6::Gui Qt6::OpenGLWidgets Qt6::Widgets + # Qt5::Core Qt5::Gui Qt5::Widgets + ) + else() + target_sources(solvespace PRIVATE + platform/guigtk.cpp) + + target_include_directories(solvespace SYSTEM PRIVATE + ${GTKMM_INCLUDE_DIRS} + ${JSONC_INCLUDE_DIRS} + ${FONTCONFIG_INCLUDE_DIRS}) + target_link_directories(solvespace PRIVATE + ${GTKMM_LIBRARY_DIRS} + ${JSONC_LIBRARY_DIRS} + ${FONTCONFIG_LIBRARY_DIRS}) + target_link_libraries(solvespace PRIVATE + ${GTKMM_LIBRARIES} + ${JSONC_LIBRARIES} + ${FONTCONFIG_LIBRARIES}) + endif() + + if(WIN32) + if(MSVC) + set_target_properties(solvespace PROPERTIES + LINK_FLAGS "/MANIFEST:NO /SAFESEH:NO /INCREMENTAL:NO /OPT:REF") + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND MSVC_VERSION) + # Hack to fix broken build with clang (not clang-cl) on Windows with CMake >= 3.22 + # which unconditionally adds "-Xlinker /MANIFEST:EMBED" to the command line + # for non-MSVC and non-MINGW compilers, and breaks the clang build where + # manifest are specified through resource files. + # See: + # https://gitlab.kitware.com/cmake/cmake/-/merge_requests/6929 + # https://gitlab.kitware.com/cmake/cmake/-/merge_requests/6534 + # https://gitlab.kitware.com/cmake/cmake/-/issues/22611 + string(REPLACE " -Xlinker /MANIFEST:EMBED" "" + CMAKE_CXX_LINK_EXECUTABLE "${CMAKE_CXX_LINK_EXECUTABLE}") + target_link_options(solvespace PRIVATE + "SHELL:-Xlinker /MANIFEST:NO") + endif() + endif() +endif() + +if(ENABLE_CLI) +# solvespace headless library + +add_library(solvespace-headless STATIC EXCLUDE_FROM_ALL + ${solvespace_core_gl_SOURCES} + platform/guinone.cpp + render/rendercairo.cpp) + +target_compile_definitions(solvespace-headless + PRIVATE HEADLESS) + +target_include_directories(solvespace-headless + INTERFACE ${CMAKE_CURRENT_SOURCE_DIR} + PUBLIC ${EIGEN3_INCLUDE_DIRS}) + +target_link_libraries(solvespace-headless + PRIVATE + solvespace-core) + +# solvespace command-line executable + + add_executable(solvespace-cli + platform/entrycli.cpp + $) + + target_link_libraries(solvespace-cli + solvespace-core + solvespace-headless) + + add_dependencies(solvespace-cli + resources) + + if(MSVC) + set_target_properties(solvespace-cli PROPERTIES + LINK_FLAGS "/INCREMENTAL:NO /OPT:REF") + endif() +endif() + +# solvespace unix package + +if(NOT (WIN32 OR APPLE OR EMSCRIPTEN)) + if(ENABLE_GUI) + install(TARGETS solvespace + COMPONENT solvespace + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + endif() + if(ENABLE_CLI) + install(TARGETS solvespace-cli + COMPONENT solvespace + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + endif() +endif() + +# solvespace macOS package + +if(APPLE) + set(LIBOMP_LIB_PATH ${OpenMP_CXX_INCLUDE_DIRS}/../lib/libomp.dylib) + set(LIBOMP_LINK_PATH "@executable_path/../Resources/libomp.dylib") + set(LIBOMP_LINK_PATH_UTILS "@executable_path/SolveSpace.app/Contents/Resources/libomp.dylib") + if(ENABLE_GUI) + add_custom_command(TARGET solvespace POST_BUILD + COMMAND cp -r ${CMAKE_BINARY_DIR}/Resources $ + ) + if(ENABLE_OPENMP) + execute_process(COMMAND install_name_tool -id ${LIBOMP_LINK_PATH} ${LIBOMP_LIB_PATH}) + message("FROM " ${${LIBOMP_LIB_PATH}} "TO" $/Resources/libomp.dylib) + add_custom_command(TARGET solvespace POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${LIBOMP_LIB_PATH} $/Resources/libomp.dylib + COMMAND install_name_tool -change ${LIBOMP_LINK_PATH} ${LIBOMP_LINK_PATH_UTILS} $ + ) + endif() + endif() + if(ENABLE_TESTS AND ENABLE_OPENMP) + add_custom_command(TARGET solvespace POST_BUILD + COMMAND install_name_tool -change ${LIBOMP_LINK_PATH} ${LIBOMP_LINK_PATH_UTILS} $) + endif() + if(ENABLE_CLI) + add_custom_command(TARGET solvespace POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy $ $ + COMMENT "Bundling executable solvespace-cli" + VERBATIM) + endif() +endif() + +add_subdirectory(slvs) diff --git a/src/bsp.cpp b/src/bsp.cpp new file mode 100644 index 0000000..50a3bb2 --- /dev/null +++ b/src/bsp.cpp @@ -0,0 +1,733 @@ +//----------------------------------------------------------------------------- +// Binary space partitioning tree, used to represent a volume in 3-space +// bounded by a triangle mesh. These are used to compute Boolean operations +// on meshes. These aren't used for anything relating to an SShell of +// ratpoly surfaces. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +SBsp2 *SBsp2::Alloc() { return (SBsp2 *)Platform::AllocTemporary(sizeof(SBsp2)); } +SBsp3 *SBsp3::Alloc() { return (SBsp3 *)Platform::AllocTemporary(sizeof(SBsp3)); } + +SBsp3 *SBsp3::FromMesh(const SMesh *m) { + SMesh mc = {}; + for(auto const &elt : m->l) { mc.AddTriangle(&elt); } + + srand(0); // Let's be deterministic, at least! + int n = mc.l.n; + while(n > 1) { + int k = rand() % n; + n--; + swap(mc.l[k], mc.l[n]); + } + + SBsp3 *bsp3 = NULL; + for(auto &elt : mc.l) { bsp3 = InsertOrCreate(bsp3, &elt, NULL); } + mc.Clear(); + return bsp3; +} + +Vector SBsp3::IntersectionWith(Vector a, Vector b) const { + double da = a.Dot(n) - d; + double db = b.Dot(n) - d; + ssassert(da*db < 0, "Expected segment to intersect BSP node"); + + double dab = (db - da); + return (a.ScaledBy(db/dab)).Plus(b.ScaledBy(-da/dab)); +} + +void SBsp3::InsertInPlane(bool pos2, STriangle *tr, SMesh *m) { + Vector tc = ((tr->a).Plus(tr->b).Plus(tr->c)).ScaledBy(1.0/3); + + bool onFace = false; + bool sameNormal = false; + double maxNormalMag = -1; + + Vector lln, trn = tr->Normal(); + + SBsp3 *ll = this; + while(ll) { + if((ll->tri).ContainsPoint(tc)) { + onFace = true; + // If the mesh contains almost-zero-area triangles, and we're + // just on the edge of one of those, then don't trust its normal. + lln = (ll->tri).Normal(); + if(lln.Magnitude() > maxNormalMag) { + sameNormal = trn.Dot(lln) > 0; + maxNormalMag = lln.Magnitude(); + } + } + ll = ll->more; + } + + if((!onFace && ((m->keepInsideOtherShell && !pos2) || + (!m->keepInsideOtherShell && pos2))) || + (onFace && ((m->keepCoplanar && m->flipNormal && !sameNormal) || + (m->keepCoplanar && !m->flipNormal && sameNormal)))) { + // We have decided that we need to keep a triangle either inside, + // outside or on the other shell. So add it and flip it if requested. + if(!(m->flipNormal)) { + m->AddTriangle(tr->meta, tr->a, tr->b, tr->c); + } else { + m->AddTriangle(tr->meta, tr->c, tr->b, tr->a); + } + } else { + m->atLeastOneDiscarded = true; + } +} + +void SBsp3::InsertHow(BspClass how, STriangle *tr, SMesh *instead) { + switch(how) { + case BspClass::POS: + if(instead && !pos) goto alt; + pos = InsertOrCreate(pos, tr, instead); + break; + + case BspClass::NEG: + if(instead && !neg) goto alt; + neg = InsertOrCreate(neg, tr, instead); + break; + + case BspClass::COPLANAR: { + if(instead) goto alt; + SBsp3 *m = Alloc(); + m->n = n; + m->d = d; + m->tri = *tr; + m->more = more; + more = m; + break; + } + } + return; + +alt: + if(((BspClass::POS == how) && !instead->keepInsideOtherShell) || + ((BspClass::NEG == how) && instead->keepInsideOtherShell)) { + // We have decided that we need to keep a triangle (either inside or + // outside the other shell. So add it and flip it if requested. + if(!(instead->flipNormal)) { + instead->AddTriangle(tr->meta, tr->a, tr->b, tr->c); + } else { + instead->AddTriangle(tr->meta, tr->c, tr->b, tr->a); + } + } else if(how == BspClass::COPLANAR) { + if(edges) { + edges->InsertTriangle(tr, instead, this); + } else { + // I suppose this actually is allowed to happen, if the coplanar + // face is the leaf, and all of its neighbors are earlier in tree? + InsertInPlane(/*pos2=*/false, tr, instead); + } + } else { + instead->atLeastOneDiscarded = true; + } +} + +class BspUtil { +public: + SBsp3 *bsp; + + size_t onc; + size_t posc; + size_t negc; + bool *isPos; + bool *isNeg; + bool *isOn; + + // triangle operations + STriangle *tr; + STriangle *btri; // also as alone + STriangle *ctri; + + // convex operations + Vector *on; + size_t npos; + size_t nneg; + Vector *vpos; // also as quad + Vector *vneg; + + static BspUtil *Alloc() { + return (BspUtil *)Platform::AllocTemporary(sizeof(BspUtil)); + } + + void AllocOn() { + on = (Vector *)Platform::AllocTemporary(sizeof(Vector) * 2); + } + + void AllocTriangle() { + btri = (STriangle *)Platform::AllocTemporary(sizeof(STriangle)); + } + + void AllocTriangles() { + btri = (STriangle *)Platform::AllocTemporary(sizeof(STriangle) * 2); + ctri = &btri[1]; + } + + void AllocQuad() { + vpos = (Vector *)Platform::AllocTemporary(sizeof(Vector) * 4); + } + + void AllocClassify(size_t size) { + // Allocate a one big piece is faster than a small ones. + isPos = (bool *)Platform::AllocTemporary(sizeof(bool) * size * 3); + isNeg = &isPos[size]; + isOn = &isNeg[size]; + } + + void AllocVertices(size_t size) { + vpos = (Vector *)Platform::AllocTemporary(sizeof(Vector) * size * 2); + vneg = &vpos[size]; + } + + void ClassifyTriangle(STriangle *tri, SBsp3 *node) { + tr = tri; + bsp = node; + onc = 0; + posc = 0; + negc = 0; + + AllocClassify(3); + + double dt[3] = { (tr->a).Dot(bsp->n), (tr->b).Dot(bsp->n), (tr->c).Dot(bsp->n) }; + double d = bsp->d; + // Count vertices in the plane + for(int i = 0; i < 3; i++) { + if(dt[i] > d + LENGTH_EPS) { + posc++; + isPos[i] = true; + } else if(dt[i] < d - LENGTH_EPS) { + negc++; + isNeg[i] = true; + } else { + onc++; + isOn[i] = true; + } + } + } + + bool ClassifyConvex(Vector *vertex, size_t cnt, SBsp3 *node, bool insertEdge) { + bsp = node; + onc = 0; + posc = 0; + negc = 0; + + AllocClassify(cnt); + AllocOn(); + + for(size_t i = 0; i < cnt; i++) { + double dt = bsp->n.Dot(vertex[i]); + isPos[i] = isNeg[i] = isOn[i] = false; + if(fabs(dt - bsp->d) < LENGTH_EPS) { + isOn[i] = true; + if(onc < 2) { + on[onc] = vertex[i]; + } + onc++; + } else if(dt > bsp->d) { + isPos[i] = true; + posc++; + } else { + isNeg[i] = true; + negc++; + } + } + + if(onc != 2 && onc != 1 && onc != 0) return false; + if(onc == 2) { + if(insertEdge) { + Vector e01 = (vertex[1]).Minus(vertex[0]); + Vector e12 = (vertex[2]).Minus(vertex[1]); + Vector out = e01.Cross(e12); + SEdge se = SEdge::From(on[0], on[1]); + bsp->edges = SBsp2::InsertOrCreateEdge(bsp->edges, &se, bsp->n, out); + } + } + return true; + } + + bool ClassifyConvexVertices(Vector *vertex, size_t cnt, bool insertEdges) { + Vector inter[2]; + int inters = 0; + + npos = 0; + nneg = 0; + + // Enlarge vertices list to consider two intersections + AllocVertices(cnt + 4); + + for(size_t i = 0; i < cnt; i++) { + size_t ip = WRAP((i + 1), cnt); + + if(isPos[i]) { + vpos[npos++] = vertex[i]; + } + if(isNeg[i]) { + vneg[nneg++] = vertex[i]; + } + if(isOn[i]) { + vneg[nneg++] = vertex[i]; + vpos[npos++] = vertex[i]; + } + if((isPos[i] && isNeg[ip]) || (isNeg[i] && isPos[ip])) { + Vector vi = bsp->IntersectionWith(vertex[i], vertex[ip]); + vpos[npos++] = vi; + vneg[nneg++] = vi; + + if(inters >= 2) return false; // triangulate: XXX shouldn't happen but does + inter[inters++] = vi; + } + } + ssassert(npos <= cnt + 1 && nneg <= cnt + 1, "Impossible"); + + if(insertEdges) { + Vector e01 = (vertex[1]).Minus(vertex[0]); + Vector e12 = (vertex[2]).Minus(vertex[1]); + Vector out = e01.Cross(e12); + if(inters == 2) { + SEdge se = SEdge::From(inter[0], inter[1]); + bsp->edges = SBsp2::InsertOrCreateEdge(bsp->edges, &se, bsp->n, out); + } else if(inters == 1 && onc == 1) { + SEdge se = SEdge::From(inter[0], on[0]); + bsp->edges = SBsp2::InsertOrCreateEdge(bsp->edges, &se, bsp->n, out); + } else if(inters == 0 && onc == 2) { + // We already handled this on-plane existing edge + } else { + return false; //triangulate; + } + } + if(nneg < 3 || npos < 3) return false; // triangulate; // XXX + + return true; + } + + void ProcessEdgeInsert() { + ssassert(onc == 2, "Impossible"); + + Vector a, b; + if (!isOn[0]) { a = tr->b; b = tr->c; } + else if(!isOn[1]) { a = tr->c; b = tr->a; } + else if(!isOn[2]) { a = tr->a; b = tr->b; } + else ssassert(false, "Impossible"); + + SEdge se = SEdge::From(a, b); + bsp->edges = SBsp2::InsertOrCreateEdge(bsp->edges, &se, bsp->n, tr->Normal()); + } + + bool SplitIntoTwoTriangles(bool insertEdge) { + ssassert(posc == 1 && negc == 1 && onc == 1, "Impossible"); + + bool bpos; + Vector a, b, c; + + // Standardize so that a is on the plane + if (isOn[0]) { a = tr->a; b = tr->b; c = tr->c; bpos = isPos[1]; + } else if(isOn[1]) { a = tr->b; b = tr->c; c = tr->a; bpos = isPos[2]; + } else if(isOn[2]) { a = tr->c; b = tr->a; c = tr->b; bpos = isPos[0]; + } else ssassert(false, "Impossible"); + + AllocTriangles(); + Vector bPc = bsp->IntersectionWith(b, c); + *btri = STriangle::From(tr->meta, a, b, bPc); + *ctri = STriangle::From(tr->meta, c, a, bPc); + + if(insertEdge) { + SEdge se = SEdge::From(a, bPc); + bsp->edges = SBsp2::InsertOrCreateEdge(bsp->edges, &se, bsp->n, tr->Normal()); + } + + return bpos; + } + + bool SplitIntoTwoPieces(bool insertEdge) { + Vector a, b, c; + if(posc == 2 && negc == 1) { + // Standardize so that a is on one side, and b and c are on the other. + if (isNeg[0]) { a = tr->a; b = tr->b; c = tr->c; + } else if(isNeg[1]) { a = tr->b; b = tr->c; c = tr->a; + } else if(isNeg[2]) { a = tr->c; b = tr->a; c = tr->b; + } else ssassert(false, "Impossible"); + } else if(posc == 1 && negc == 2) { + if (isPos[0]) { a = tr->a; b = tr->b; c = tr->c; + } else if(isPos[1]) { a = tr->b; b = tr->c; c = tr->a; + } else if(isPos[2]) { a = tr->c; b = tr->a; c = tr->b; + } else ssassert(false, "Impossible"); + } else ssassert(false, "Impossible"); + + Vector aPb = bsp->IntersectionWith(a, b); + Vector cPa = bsp->IntersectionWith(c, a); + AllocTriangle(); + AllocQuad(); + + *btri = STriangle::From(tr->meta, a, aPb, cPa); + + vpos[0] = aPb; + vpos[1] = b; + vpos[2] = c; + vpos[3] = cPa; + + if(insertEdge) { + SEdge se = SEdge::From(aPb, cPa); + bsp->edges = SBsp2::InsertOrCreateEdge(bsp->edges, &se, bsp->n, btri->Normal()); + } + + return posc == 2 && negc == 1; + } + + static SBsp3 *Triangulate(SBsp3 *bsp, const STriMeta &meta, Vector *vertex, + size_t cnt, SMesh *instead) { + for(size_t i = 0; i < cnt - 2; i++) { + STriangle tr = STriangle::From(meta, vertex[0], vertex[i + 1], vertex[i + 2]); + bsp = SBsp3::InsertOrCreate(bsp, &tr, instead); + } + return bsp; + } +}; + +void SBsp3::InsertConvexHow(BspClass how, STriMeta meta, Vector *vertex, size_t n, + SMesh *instead) { + switch(how) { + case BspClass::POS: + if(pos) { + pos = pos->InsertConvex(meta, vertex, n, instead); + return; + } + break; + + case BspClass::NEG: + if(neg) { + neg = neg->InsertConvex(meta, vertex, n, instead); + return; + } + break; + + default: ssassert(false, "Unexpected BSP insert type"); + } + + for(size_t i = 0; i < n - 2; i++) { + STriangle tr = STriangle::From(meta, + vertex[0], vertex[i+1], vertex[i+2]); + InsertHow(how, &tr, instead); + } +} + +SBsp3 *SBsp3::InsertConvex(STriMeta meta, Vector *vertex, size_t cnt, SMesh *instead) { + BspUtil *u = BspUtil::Alloc(); + if(u->ClassifyConvex(vertex, cnt, this, !instead)) { + if(u->posc == 0) { + InsertConvexHow(BspClass::NEG, meta, vertex, cnt, instead); + return this; + } + if(u->negc == 0) { + InsertConvexHow(BspClass::POS, meta, vertex, cnt, instead); + return this; + } + + if(u->ClassifyConvexVertices(vertex, cnt, !instead)) { + InsertConvexHow(BspClass::NEG, meta, u->vneg, u->nneg, instead); + InsertConvexHow(BspClass::POS, meta, u->vpos, u->npos, instead); + return this; + } + } + + // We don't handle the special case for this; do it as triangles + return BspUtil::Triangulate(this, meta, vertex, cnt, instead); +} + +SBsp3 *SBsp3::InsertOrCreate(SBsp3 *where, STriangle *tr, SMesh *instead) { + if(where == NULL) { + if(instead) { + // ruevs: I do not think this code is reachable, but in + // principle should we use instead->keepInsideOtherShell + // in place of instead->flipNormal ? + if(instead->flipNormal) { + instead->atLeastOneDiscarded = true; + } else { + instead->AddTriangle(tr->meta, tr->a, tr->b, tr->c); + } + return NULL; + } + + // Brand new node; so allocate for it, and fill us in. + SBsp3 *r = Alloc(); + r->n = (tr->Normal()).WithMagnitude(1); + r->d = (tr->a).Dot(r->n); + r->tri = *tr; + return r; + } + where->Insert(tr, instead); + return where; +} + +void SBsp3::Insert(STriangle *tr, SMesh *instead) { + BspUtil *u = BspUtil::Alloc(); + u->ClassifyTriangle(tr, this); + + // All vertices in-plane + if(u->onc == 3) { + InsertHow(BspClass::COPLANAR, tr, instead); + return; + } + + // No split required + if(u->posc == 0 || u->negc == 0) { + if(!instead && u->onc == 2) { + u->ProcessEdgeInsert(); + } + + if(u->posc > 0) { + InsertHow(BspClass::POS, tr, instead); + } else { + InsertHow(BspClass::NEG, tr, instead); + } + return; + } + + // The polygon must be split into two triangles, one above, one below. + if(u->posc == 1 && u->negc == 1 && u->onc == 1) { + if(u->SplitIntoTwoTriangles(!instead)) { + InsertHow(BspClass::POS, u->btri, instead); + InsertHow(BspClass::NEG, u->ctri, instead); + } else { + InsertHow(BspClass::POS, u->ctri, instead); + InsertHow(BspClass::NEG, u->btri, instead); + } + return; + } + + // The polygon must be split into two pieces: a triangle and a quad. + if(u->SplitIntoTwoPieces(!instead)) { + InsertConvexHow(BspClass::POS, tr->meta, u->vpos, 4, instead); + InsertHow(BspClass::NEG, u->btri, instead); + } else { + InsertConvexHow(BspClass::NEG, tr->meta, u->vpos, 4, instead); + InsertHow(BspClass::POS, u->btri, instead); + } +} + +void SBsp3::GenerateInPaintOrder(SMesh *m) const { + // Doesn't matter which branch we take if the normal has zero z + // component, so don't need a separate case for that. + if(n.z < 0) { + if(pos) pos->GenerateInPaintOrder(m); + } else { + if(neg) neg->GenerateInPaintOrder(m); + } + + const SBsp3 *flip = this; + while(flip) { + m->AddTriangle(&(flip->tri)); + flip = flip->more; + } + + if(n.z < 0) { + if(neg) neg->GenerateInPaintOrder(m); + } else { + if(pos) pos->GenerateInPaintOrder(m); + } +} + +///////////////////////////////// + +Vector SBsp2::IntersectionWith(Vector a, Vector b) const { + double da = a.Dot(no) - d; + double db = b.Dot(no) - d; + ssassert(da*db < 0, "Expected segment to intersect BSP node"); + + double dab = (db - da); + return (a.ScaledBy(db/dab)).Plus(b.ScaledBy(-da/dab)); +} + +SBsp2 *SBsp2::InsertOrCreateEdge(SBsp2 *where, SEdge *nedge, Vector nnp, Vector out) { + if(where == NULL) { + // Brand new node; so allocate for it, and fill us in. + SBsp2 *r = Alloc(); + r->np = nnp; + r->no = ((r->np).Cross((nedge->b).Minus(nedge->a))).WithMagnitude(1); + if(out.Dot(r->no) < 0) { + r->no = (r->no).ScaledBy(-1); + } + r->d = (nedge->a).Dot(r->no); + r->edge = *nedge; + return r; + } + where->InsertEdge(nedge, nnp, out); + return where; +} + +void SBsp2::InsertEdge(SEdge *nedge, Vector nnp, Vector out) { + + double dt[2] = { (nedge->a).Dot(no), (nedge->b).Dot(no) }; + + bool isPos[2] = {}, isNeg[2] = {}, isOn[2] = {}; + for(int i = 0; i < 2; i++) { + if(fabs(dt[i] - d) < LENGTH_EPS) { + isOn[i] = true; + } else if(dt[i] > d) { + isPos[i] = true; + } else { + isNeg[i] = true; + } + } + + if((isPos[0] && isPos[1])||(isPos[0] && isOn[1])||(isOn[0] && isPos[1])) { + pos = InsertOrCreateEdge(pos, nedge, nnp, out); + return; + } + if((isNeg[0] && isNeg[1])||(isNeg[0] && isOn[1])||(isOn[0] && isNeg[1])) { + neg = InsertOrCreateEdge(neg, nedge, nnp, out); + return; + } + if(isOn[0] && isOn[1]) { + SBsp2 *m = Alloc(); + + m->np = nnp; + m->no = ((m->np).Cross((nedge->b).Minus(nedge->a))).WithMagnitude(1); + if(out.Dot(m->no) < 0) { + m->no = (m->no).ScaledBy(-1); + } + m->d = (nedge->a).Dot(m->no); + m->edge = *nedge; + + m->more = more; + more = m; + return; + } + if((isPos[0] && isNeg[1]) || (isNeg[0] && isPos[1])) { + Vector aPb = IntersectionWith(nedge->a, nedge->b); + + SEdge ea = SEdge::From(nedge->a, aPb); + SEdge eb = SEdge::From(aPb, nedge->b); + + if(isPos[0]) { + pos = InsertOrCreateEdge(pos, &ea, nnp, out); + neg = InsertOrCreateEdge(neg, &eb, nnp, out); + } else { + neg = InsertOrCreateEdge(neg, &ea, nnp, out); + pos = InsertOrCreateEdge(pos, &eb, nnp, out); + } + return; + } + ssassert(false, "Impossible"); +} + +void SBsp2::InsertTriangleHow(BspClass how, STriangle *tr, SMesh *m, SBsp3 *bsp3) { + switch(how) { + case BspClass::POS: + if(pos) { + pos->InsertTriangle(tr, m, bsp3); + } else { + bsp3->InsertInPlane(/*pos2=*/true, tr, m); + } + break; + + case BspClass::NEG: + if(neg) { + neg->InsertTriangle(tr, m, bsp3); + } else { + bsp3->InsertInPlane(/*pos2=*/false, tr, m); + } + break; + + default: ssassert(false, "Unexpected BSP insert type"); + } +} + +void SBsp2::InsertTriangle(STriangle *tr, SMesh *m, SBsp3 *bsp3) { + double dt[3] = { (tr->a).Dot(no), (tr->b).Dot(no), (tr->c).Dot(no) }; + + bool isPos[3] = {}, isNeg[3] = {}, isOn[3] = {}; + int inc = 0, posc = 0, negc = 0; + for(int i = 0; i < 3; i++) { + if(fabs(dt[i] - d) < LENGTH_EPS) { + isOn[i] = true; + inc++; + } else if(dt[i] > d) { + isPos[i] = true; + posc++; + } else { + isNeg[i] = true; + negc++; + } + } + + if(inc == 3) { + // All vertices on-line; so it's a degenerate triangle, to ignore. + return; + } + + // No split required + if(posc == 0 || negc == 0) { + if(posc > 0) { + InsertTriangleHow(BspClass::POS, tr, m, bsp3); + } else { + InsertTriangleHow(BspClass::NEG, tr, m, bsp3); + } + return; + } + + // The polygon must be split into two pieces, one above, one below. + Vector a, b, c; + + if(posc == 1 && negc == 1 && inc == 1) { + bool bpos; + // Standardize so that a is on the plane + if (isOn[0]) { a = tr->a; b = tr->b; c = tr->c; bpos = isPos[1]; + } else if(isOn[1]) { a = tr->b; b = tr->c; c = tr->a; bpos = isPos[2]; + } else if(isOn[2]) { a = tr->c; b = tr->a; c = tr->b; bpos = isPos[0]; + } else ssassert(false, "Impossible"); + + Vector bPc = IntersectionWith(b, c); + STriangle btri = STriangle::From(tr->meta, a, b, bPc); + STriangle ctri = STriangle::From(tr->meta, c, a, bPc); + + if(bpos) { + InsertTriangleHow(BspClass::POS, &btri, m, bsp3); + InsertTriangleHow(BspClass::NEG, &ctri, m, bsp3); + } else { + InsertTriangleHow(BspClass::POS, &ctri, m, bsp3); + InsertTriangleHow(BspClass::NEG, &btri, m, bsp3); + } + + return; + } + + if(posc == 2 && negc == 1) { + // Standardize so that a is on one side, and b and c are on the other. + if (isNeg[0]) { a = tr->a; b = tr->b; c = tr->c; + } else if(isNeg[1]) { a = tr->b; b = tr->c; c = tr->a; + } else if(isNeg[2]) { a = tr->c; b = tr->a; c = tr->b; + } else ssassert(false, "Impossible"); + + } else if(posc == 1 && negc == 2) { + if (isPos[0]) { a = tr->a; b = tr->b; c = tr->c; + } else if(isPos[1]) { a = tr->b; b = tr->c; c = tr->a; + } else if(isPos[2]) { a = tr->c; b = tr->a; c = tr->b; + } else ssassert(false, "Impossible"); + } else ssassert(false, "Impossible"); + + Vector aPb = IntersectionWith(a, b); + Vector cPa = IntersectionWith(c, a); + + STriangle alone = STriangle::From(tr->meta, a, aPb, cPa); + STriangle quad1 = STriangle::From(tr->meta, aPb, b, c ); + STriangle quad2 = STriangle::From(tr->meta, aPb, c, cPa); + + if(posc == 2 && negc == 1) { + InsertTriangleHow(BspClass::POS, &quad1, m, bsp3); + InsertTriangleHow(BspClass::POS, &quad2, m, bsp3); + InsertTriangleHow(BspClass::NEG, &alone, m, bsp3); + } else { + InsertTriangleHow(BspClass::NEG, &quad1, m, bsp3); + InsertTriangleHow(BspClass::NEG, &quad2, m, bsp3); + InsertTriangleHow(BspClass::POS, &alone, m, bsp3); + } + + return; +} + +} // namespace SolveSpace diff --git a/src/clipboard.cpp b/src/clipboard.cpp new file mode 100644 index 0000000..8eea1a8 --- /dev/null +++ b/src/clipboard.cpp @@ -0,0 +1,527 @@ +//----------------------------------------------------------------------------- +// The clipboard that gets manipulated when the user selects Edit -> Cut, +// Copy, Paste, etc.; may contain entities only, not constraints. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +void SolveSpaceUI::Clipboard::Clear() { + c.Clear(); + r.Clear(); +} + +bool SolveSpaceUI::Clipboard::ContainsEntity(hEntity he) { + if(he == Entity::NO_ENTITY) + return true; + + ClipboardRequest *cr; + for(cr = r.First(); cr; cr = r.NextAfter(cr)) { + if(cr->oldEnt == he) + return true; + + for(int i = 0; i < MAX_POINTS_IN_ENTITY; i++) { + if(cr->oldPointEnt[i] == he) + return true; + } + } + return false; +} + +hEntity SolveSpaceUI::Clipboard::NewEntityFor(hEntity he) { + if(he == Entity::NO_ENTITY) + return Entity::NO_ENTITY; + + ClipboardRequest *cr; + for(cr = r.First(); cr; cr = r.NextAfter(cr)) { + if(cr->oldEnt == he) + return cr->newReq.entity(0); + + for(int i = 0; i < MAX_POINTS_IN_ENTITY; i++) { + if(cr->oldPointEnt[i] == he) + return cr->newReq.entity(1+i); + } + } + + ssassert(false, "Expected to find entity in some clipboard request"); +} + +void GraphicsWindow::DeleteSelection() { + SK.request.ClearTags(); + SK.constraint.ClearTags(); + List *ls = &(selection); + for(Selection *s = ls->First(); s; s = ls->NextAfter(s)) { + hRequest r = { 0 }; + if(s->entity.v && s->entity.isFromRequest()) { + r = s->entity.request(); + } + if(r.v && !r.IsFromReferences()) { + SK.request.Tag(r, 1); + } + if(s->constraint.v) { + SK.constraint.Tag(s->constraint, 1); + } + } + + SK.constraint.RemoveTagged(); + // Note that this regenerates and clears the selection, to avoid + // lingering references to the just-deleted items. + DeleteTaggedRequests(); +} + +void GraphicsWindow::CopySelection() { + SS.clipboard.Clear(); + + Entity *wrkpl = SK.GetEntity(ActiveWorkplane()); + Entity *wrkpln = SK.GetEntity(wrkpl->normal); + Vector u = wrkpln->NormalU(), + v = wrkpln->NormalV(), + n = wrkpln->NormalN(), + p = SK.GetEntity(wrkpl->point[0])->PointGetNum(); + + List *ls = &(selection); + for(Selection *s = ls->First(); s; s = ls->NextAfter(s)) { + if(!s->entity.v) continue; + // Work only on entities that have requests that will generate them. + Entity *e = SK.GetEntity(s->entity); + bool hasDistance; + Request::Type req; + int pts; + if(!EntReqTable::GetEntityInfo(e->type, e->extraPoints, + &req, &pts, NULL, &hasDistance)) + { + if(!e->h.isFromRequest()) continue; + Request *r = SK.GetRequest(e->h.request()); + if(r->type != Request::Type::DATUM_POINT) continue; + ssassert(EntReqTable::GetEntityInfo((Entity::Type)0, e->extraPoints, + &req, &pts, NULL, &hasDistance), "No entity info"); + } + if(req == Request::Type::WORKPLANE) continue; + + ClipboardRequest cr = {}; + cr.type = req; + cr.extraPoints = e->extraPoints; + cr.style = e->style; + cr.str = e->str; + cr.font = e->font; + cr.file = e->file; + cr.construction = e->construction; + {for(int i = 0; i < pts; i++) { + Vector pt; + if(req == Request::Type::DATUM_POINT) { + pt = e->PointGetNum(); + } else { + pt = SK.GetEntity(e->point[i])->PointGetNum(); + } + pt = pt.Minus(p); + pt = pt.DotInToCsys(u, v, n); + cr.point[i] = pt; + }} + if(hasDistance) { + cr.distance = SK.GetEntity(e->distance)->DistanceGetNum(); + } + + cr.oldEnt = e->h; + for(int i = 0; i < pts; i++) { + cr.oldPointEnt[i] = e->point[i]; + } + + SS.clipboard.r.Add(&cr); + } + + for(Selection *s = ls->First(); s; s = ls->NextAfter(s)) { + if(!s->constraint.v) continue; + + Constraint *c = SK.GetConstraint(s->constraint); + if(c->type == Constraint::Type::COMMENT) { + SS.clipboard.c.Add(c); + } + } + + for(Constraint &c : SK.constraint) { + if(!SS.clipboard.ContainsEntity(c.ptA) || + !SS.clipboard.ContainsEntity(c.ptB) || + !SS.clipboard.ContainsEntity(c.entityA) || + !SS.clipboard.ContainsEntity(c.entityB) || + !SS.clipboard.ContainsEntity(c.entityC) || + !SS.clipboard.ContainsEntity(c.entityD) || + c.type == Constraint::Type::COMMENT) { + continue; + } + SS.clipboard.c.Add(&c); + } +} + +void GraphicsWindow::PasteClipboard(Vector trans, double theta, double scale) { + Entity *wrkpl = SK.GetEntity(ActiveWorkplane()); + Entity *wrkpln = SK.GetEntity(wrkpl->normal); + Vector u = wrkpln->NormalU(), + v = wrkpln->NormalV(), + n = wrkpln->NormalN(), + p = SK.GetEntity(wrkpl->point[0])->PointGetNum(); + + // For arcs, reflection involves swapping the endpoints, or otherwise + // the arc gets inverted. + auto mapPoint = [scale](hEntity he) { + if(he.v == 0) return he; + + if(scale < 0) { + hRequest hr = he.request(); + Request *r = SK.GetRequest(hr); + if(r->type == Request::Type::ARC_OF_CIRCLE) { + if(he == hr.entity(2)) { + return hr.entity(3); + } else if(he == hr.entity(3)) { + return hr.entity(2); + } + } + } + return he; + }; + + ClipboardRequest *cr; + for(cr = SS.clipboard.r.First(); cr; cr = SS.clipboard.r.NextAfter(cr)) { + hRequest hr = AddRequest(cr->type, /*rememberForUndo=*/false); + Request *r = SK.GetRequest(hr); + r->extraPoints = cr->extraPoints; + r->style = cr->style; + r->str = cr->str; + r->font = cr->font; + r->file = cr->file; + r->construction = cr->construction; + // Need to regen to get the right number of points, if extraPoints + // changed. + SS.GenerateAll(SolveSpaceUI::Generate::REGEN); + SS.MarkGroupDirty(r->group); + bool hasDistance; + int i, pts; + EntReqTable::GetRequestInfo(r->type, r->extraPoints, + NULL, &pts, NULL, &hasDistance); + for(i = 0; i < pts; i++) { + Vector pt = cr->point[i]; + // We need the reflection to occur within the workplane; it may + // otherwise correspond to just a rotation as projected. + if(scale < 0) { + pt.x *= -1; + } + // Likewise the scale, which could otherwise take us out of the + // workplane. + pt = pt.ScaledBy(fabs(scale)); + pt = pt.ScaleOutOfCsys(u, v, Vector::From(0, 0, 0)); + pt = pt.Plus(p); + pt = pt.RotatedAbout(n, theta); + pt = pt.Plus(trans); + int j = (r->type == Request::Type::DATUM_POINT) ? i : i + 1; + SK.GetEntity(mapPoint(hr.entity(j)))->PointForceTo(pt); + } + if(hasDistance) { + SK.GetEntity(hr.entity(64))->DistanceForceTo( + cr->distance*fabs(scale)); + } + + cr->newReq = hr; + MakeSelected(hr.entity(0)); + for(i = 0; i < pts; i++) { + int j = (r->type == Request::Type::DATUM_POINT) ? i : i + 1; + MakeSelected(hr.entity(j)); + } + } + Constraint *cc; + for(cc = SS.clipboard.c.First(); cc; cc = SS.clipboard.c.NextAfter(cc)) { + Constraint c = {}; + c.group = SS.GW.activeGroup; + c.workplane = SS.GW.ActiveWorkplane(); + c.type = cc->type; + c.valA = cc->valA; + c.ptA = SS.clipboard.NewEntityFor(mapPoint(cc->ptA)); + c.ptB = SS.clipboard.NewEntityFor(mapPoint(cc->ptB)); + c.entityA = SS.clipboard.NewEntityFor(cc->entityA); + c.entityB = SS.clipboard.NewEntityFor(cc->entityB); + c.entityC = SS.clipboard.NewEntityFor(cc->entityC); + c.entityD = SS.clipboard.NewEntityFor(cc->entityD); + c.other = cc->other; + c.other2 = cc->other2; + c.reference = cc->reference; + c.disp = cc->disp; + c.comment = cc->comment; + bool dontAddConstraint = false; + switch(c.type) { + case Constraint::Type::COMMENT: + c.disp.offset = c.disp.offset.Plus(trans); + break; + case Constraint::Type::PT_LINE_DISTANCE: + c.valA *= scale; + break; + case Constraint::Type::PT_PT_DISTANCE: + case Constraint::Type::PROJ_PT_DISTANCE: + case Constraint::Type::DIAMETER: + c.valA *= fabs(scale); + break; + case Constraint::Type::ARC_LINE_TANGENT: { + if(scale < 0) { + // When mirroring swap the end point of the arc. The reason is that arcs are + // defined as counter-clockwise from the start point to the end point. And we + // swapped its points in mapPoint above to avoid inverting it. + c.other = !c.other; + } + break; + } + case Constraint::Type::CUBIC_LINE_TANGENT: { + // Nothing to do for CUBIC unlike the ARC_OF_CIRCLE above and below. + break; + } + case Constraint::Type::CURVE_CURVE_TANGENT: { + if(scale < 0) { + // When mirroring swap the end points of arcs. The reason is that arcs are + // defined as counter-clockwise from the start point to the end point. And we + // swapped their points in mapPoint above to avoid inverting them. + // CUBIC splines do not need this. + if(EntityBase::Type::ARC_OF_CIRCLE == SK.GetEntity(c.entityA)->type) { + c.other = !c.other; + } + if(EntityBase::Type::ARC_OF_CIRCLE == SK.GetEntity(c.entityB)->type) { + c.other2 = !c.other2; + } + } + break; + } + case Constraint::Type::HORIZONTAL: + case Constraint::Type::VERTICAL: + // When rotating 90 or 270 degrees, swap the vertical / horizontal constraints + if (EXACT(fmod(theta + (PI/2), PI) == 0)) { + if(c.type == Constraint::Type::HORIZONTAL) { + c.type = Constraint::Type::VERTICAL; + } else { + c.type = Constraint::Type::HORIZONTAL; + } + } else if (fmod(theta, PI/2) != 0) { + dontAddConstraint = true; + } + break; + default: + break; + } + if (!dontAddConstraint) { + hConstraint hc = Constraint::AddConstraint(&c, /*rememberForUndo=*/false); + if(c.type == Constraint::Type::COMMENT) { + MakeSelected(hc); + } + } + } +} + +void GraphicsWindow::MenuClipboard(Command id) { + if(id != Command::DELETE && !SS.GW.LockedInWorkplane()) { + Error(_("Cut, paste, and copy work only in a workplane.\n\n" + "Activate one with Sketch -> In Workplane.")); + return; + } + + switch(id) { + case Command::PASTE: { + SS.UndoRemember(); + Vector trans = SS.GW.projRight.ScaledBy(80/SS.GW.scale).Plus( + SS.GW.projUp .ScaledBy(40/SS.GW.scale)); + SS.GW.ClearSelection(); + SS.GW.PasteClipboard(trans, 0, 1); + break; + } + + case Command::PASTE_TRANSFORM: { + if(SS.clipboard.r.IsEmpty()) { + Error(_("Clipboard is empty; nothing to paste.")); + break; + } + + Entity *wrkpl = SK.GetEntity(SS.GW.ActiveWorkplane()); + Vector p = SK.GetEntity(wrkpl->point[0])->PointGetNum(); + SS.TW.shown.paste.times = 1; + SS.TW.shown.paste.trans = Vector::From(0, 0, 0); + SS.TW.shown.paste.theta = 0; + SS.TW.shown.paste.origin = p; + SS.TW.shown.paste.scale = 1; + SS.TW.GoToScreen(TextWindow::Screen::PASTE_TRANSFORMED); + SS.GW.ForceTextWindowShown(); + SS.ScheduleShowTW(); + break; + } + + case Command::COPY: + SS.GW.CopySelection(); + SS.GW.ClearSelection(); + break; + + case Command::CUT: + SS.UndoRemember(); + SS.GW.CopySelection(); + SS.GW.DeleteSelection(); + break; + + case Command::DELETE: + SS.UndoRemember(); + SS.GW.DeleteSelection(); + break; + + default: ssassert(false, "Unexpected menu ID"); + } +} + +bool TextWindow::EditControlDoneForPaste(const std::string &s) { + Expr *e; + switch(edit.meaning) { + case Edit::PASTE_TIMES_REPEATED: { + e = Expr::From(s, /*popUpError=*/true); + if(!e) break; + int v = (int)e->Eval(); + if(v > 0) { + shown.paste.times = v; + } else { + Error(_("Number of copies to paste must be at least one.")); + } + break; + } + case Edit::PASTE_ANGLE: + e = Expr::From(s, /*popUpError=*/true); + if(!e) break; + shown.paste.theta = WRAP_SYMMETRIC((e->Eval())*PI/180, 2*PI); + break; + + case Edit::PASTE_SCALE: { + e = Expr::From(s, /*popUpError=*/true); + double v = e->Eval(); + if(fabs(v) > 1e-6) { + shown.paste.scale = shown.paste.scale < 0 ? -v : v; + } else { + Error(_("Scale cannot be zero.")); + } + break; + } + + default: + return false; + } + return true; +} + +void TextWindow::ScreenChangePasteTransformed(int link, uint32_t v) { + switch(link) { + case 't': + SS.TW.ShowEditControl(13, ssprintf("%d", SS.TW.shown.paste.times)); + SS.TW.edit.meaning = Edit::PASTE_TIMES_REPEATED; + break; + + case 'r': + SS.TW.ShowEditControl(13, ssprintf("%.3f", SS.TW.shown.paste.theta*180/PI)); + SS.TW.edit.meaning = Edit::PASTE_ANGLE; + break; + + case 's': + SS.TW.ShowEditControl(13, ssprintf("%.3f", fabs(SS.TW.shown.paste.scale))); + SS.TW.edit.meaning = Edit::PASTE_SCALE; + break; + + case 'f': + SS.TW.shown.paste.scale *= -1; + break; + } +} + +void TextWindow::ScreenPasteTransformed(int link, uint32_t v) { + SS.GW.GroupSelection(); + switch(link) { + case 'o': + if(SS.GW.gs.points == 1 && SS.GW.gs.n == 1) { + Entity *e = SK.GetEntity(SS.GW.gs.point[0]); + SS.TW.shown.paste.origin = e->PointGetNum(); + } else { + Error(_("Select one point to define origin of rotation.")); + } + SS.GW.ClearSelection(); + break; + + case 't': + if(SS.GW.gs.points == 2 && SS.GW.gs.n == 2) { + Entity *pa = SK.GetEntity(SS.GW.gs.point[0]), + *pb = SK.GetEntity(SS.GW.gs.point[1]); + SS.TW.shown.paste.trans = + (pb->PointGetNum()).Minus(pa->PointGetNum()); + } else { + Error(_("Select two points to define translation vector.")); + } + SS.GW.ClearSelection(); + break; + + case 'g': { + if(fabs(SS.TW.shown.paste.theta) < LENGTH_EPS && + SS.TW.shown.paste.trans.Magnitude() < LENGTH_EPS && + SS.TW.shown.paste.times != 1) + { + Message(_("Transformation is identity. So all copies will be " + "exactly on top of each other.")); + } + if(SS.TW.shown.paste.times*SS.clipboard.r.n > 100) { + Error(_("Too many items to paste; split this into smaller " + "pastes.")); + break; + } + if(!SS.GW.LockedInWorkplane()) { + Error(_("No workplane active.")); + break; + } + Entity *wrkpl = SK.GetEntity(SS.GW.ActiveWorkplane()); + Entity *wrkpln = SK.GetEntity(wrkpl->normal); + Vector wn = wrkpln->NormalN(); + SS.UndoRemember(); + SS.GW.ClearSelection(); + for(int i = 0; i < SS.TW.shown.paste.times; i++) { + Vector trans = SS.TW.shown.paste.trans.ScaledBy(i+1), + origin = SS.TW.shown.paste.origin; + double theta = SS.TW.shown.paste.theta*(i+1); + // desired transformation is Q*(p - o) + o + t = + // Q*p - Q*o + o + t = Q*p + (t + o - Q*o) + Vector t = trans.Plus( + origin).Minus( + origin.RotatedAbout(wn, theta)); + + SS.GW.PasteClipboard(t, theta, SS.TW.shown.paste.scale); + } + SS.TW.GoToScreen(Screen::LIST_OF_GROUPS); + SS.ScheduleShowTW(); + break; + } + } +} + +void TextWindow::ShowPasteTransformed() { + Printf(true, "%FtPASTE TRANSFORMED%E"); + Printf(true, "%Ba %Ftrepeat%E %d time%s %Fl%Lt%f[change]%E", + shown.paste.times, (shown.paste.times == 1) ? "" : "s", + &ScreenChangePasteTransformed); + Printf(false, "%Bd %Ftrotate%E %@ degrees %Fl%Lr%f[change]%E", + shown.paste.theta*180/PI, + &ScreenChangePasteTransformed); + Printf(false, "%Ba %Ftabout pt%E (%s, %s, %s) %Fl%Lo%f[use selected]%E", + SS.MmToString(shown.paste.origin.x).c_str(), + SS.MmToString(shown.paste.origin.y).c_str(), + SS.MmToString(shown.paste.origin.z).c_str(), + &ScreenPasteTransformed); + Printf(false, "%Bd %Fttranslate%E (%s, %s, %s) %Fl%Lt%f[use selected]%E", + SS.MmToString(shown.paste.trans.x).c_str(), + SS.MmToString(shown.paste.trans.y).c_str(), + SS.MmToString(shown.paste.trans.z).c_str(), + &ScreenPasteTransformed); + Printf(false, "%Ba %Ftscale%E %@ %Fl%Ls%f[change]%E", + fabs(shown.paste.scale), + &ScreenChangePasteTransformed); + Printf(false, "%Ba %Ftmirror%E %Fd%Lf%f%s flip%E", + &ScreenChangePasteTransformed, + shown.paste.scale < 0 ? CHECK_TRUE : CHECK_FALSE); + + Printf(true, " %Fl%Lg%fpaste transformed now%E", &ScreenPasteTransformed); + + Printf(true, "(or %Fl%Ll%fcancel operation%E)", &ScreenHome); +} + +} // namespace SolveSpace diff --git a/src/config.h.in b/src/config.h.in new file mode 100644 index 0000000..187b9cb --- /dev/null +++ b/src/config.h.in @@ -0,0 +1,19 @@ +#ifndef SOLVESPACE_CONFIG_H +#define SOLVESPACE_CONFIG_H + +#define PACKAGE_VERSION "@PROJECT_VERSION@~@solvespace_GIT_HASH@" +#define GIT_HASH_URL "https://github.com/solvespace/solvespace/commit/@solvespace_GIT_HASH@" + +/* Non-OS X *nix only */ +#define UNIX_DATADIR "@CMAKE_INSTALL_PREFIX@/@CMAKE_INSTALL_DATAROOTDIR@/solvespace" + +/* Do we have the si library on win32, or libspnav on *nix? */ +#cmakedefine HAVE_SPACEWARE + +/* What OpenGL version do we use? */ +#define HAVE_OPENGL @OPENGL@ + +/* If we use GTK, can we use the native file chooser? */ +#cmakedefine HAVE_GTK_FILECHOOSERNATIVE + +#endif diff --git a/src/confscreen.cpp b/src/confscreen.cpp new file mode 100644 index 0000000..7cd86fd --- /dev/null +++ b/src/confscreen.cpp @@ -0,0 +1,592 @@ +//----------------------------------------------------------------------------- +// For the configuration screen, setup items that are not specific to the +// file being edited right now. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" +#if defined(_OPENMP) +#include +#endif + +namespace SolveSpace { + +void TextWindow::ScreenChangeColor(int link, uint32_t v) { + SS.TW.ShowEditControlWithColorPicker(13, SS.modelColor[v]); + + SS.TW.edit.meaning = Edit::COLOR; + SS.TW.edit.i = v; +} + +void TextWindow::ScreenChangeChordTolerance(int link, uint32_t v) { + SS.TW.ShowEditControl(3, ssprintf("%lg", SS.chordTol)); + SS.TW.edit.meaning = Edit::CHORD_TOLERANCE; + SS.TW.edit.i = 0; +} + +void TextWindow::ScreenChangeMaxSegments(int link, uint32_t v) { + SS.TW.ShowEditControl(3, ssprintf("%d", SS.maxSegments)); + SS.TW.edit.meaning = Edit::MAX_SEGMENTS; + SS.TW.edit.i = 0; +} + +void TextWindow::ScreenChangeExportChordTolerance(int link, uint32_t v) { + SS.TW.ShowEditControl(3, ssprintf("%lg", SS.exportChordTol)); + SS.TW.edit.meaning = Edit::CHORD_TOLERANCE; + SS.TW.edit.i = 1; +} + +void TextWindow::ScreenChangeExportMaxSegments(int link, uint32_t v) { + SS.TW.ShowEditControl(3, ssprintf("%d", SS.exportMaxSegments)); + SS.TW.edit.meaning = Edit::MAX_SEGMENTS; + SS.TW.edit.i = 1; +} + +void TextWindow::ScreenChangeGridSpacing(int link, uint32_t v) { + SS.TW.ShowEditControl(3, SS.MmToString(SS.gridSpacing, true)); + SS.TW.edit.meaning = Edit::GRID_SPACING; +} + +void TextWindow::ScreenChangeDigitsAfterDecimal(int link, uint32_t v) { + SS.TW.ShowEditControl(14, ssprintf("%d", SS.UnitDigitsAfterDecimal())); + SS.TW.edit.meaning = Edit::DIGITS_AFTER_DECIMAL; +} + +void TextWindow::ScreenChangeDigitsAfterDecimalDegree(int link, uint32_t v) { + SS.TW.ShowEditControl(14, ssprintf("%d", SS.afterDecimalDegree)); + SS.TW.edit.meaning = Edit::DIGITS_AFTER_DECIMAL_DEGREE; +} + +void TextWindow::ScreenChangeUseSIPrefixes(int link, uint32_t v) { + SS.useSIPrefixes = !SS.useSIPrefixes; + SS.GW.Invalidate(); +} + +void TextWindow::ScreenChangeExportScale(int link, uint32_t v) { + SS.TW.ShowEditControl(5, ssprintf("%.3f", (double)SS.exportScale)); + SS.TW.edit.meaning = Edit::EXPORT_SCALE; +} + +void TextWindow::ScreenChangeExportOffset(int link, uint32_t v) { + SS.TW.ShowEditControl(3, SS.MmToString(SS.exportOffset, true)); + SS.TW.edit.meaning = Edit::EXPORT_OFFSET; +} + +void TextWindow::ScreenChangeArcDimDefault(int link, uint32_t v) { + SS.arcDimDefaultDiameter = !SS.arcDimDefaultDiameter; +} + +void TextWindow::ScreenChangeShowFullFilePath(int link, uint32_t v) { + SS.showFullFilePath = !SS.showFullFilePath; + SS.UpdateWindowTitles(); +} + +void TextWindow::ScreenChangeFixExportColors(int link, uint32_t v) { + SS.fixExportColors = !SS.fixExportColors; +} + +void TextWindow::ScreenChangeExportBackgroundColor(int link, uint32_t v) { + SS.exportBackgroundColor = !SS.exportBackgroundColor; +} + +void TextWindow::ScreenChangeBackFaces(int link, uint32_t v) { + SS.drawBackFaces = !SS.drawBackFaces; + SS.GW.Invalidate(/*clearPersistent=*/true); +} + +void TextWindow::ScreenChangeTurntableNav(int link, uint32_t v) { + SS.turntableNav = !SS.turntableNav; + if(SS.turntableNav) { + // If turntable nav is being turned on, align view so Z is vertical + SS.GW.AnimateOnto(Quaternion::From(Vector::From(-1, 0, 0), Vector::From(0, 0, 1)), + SS.GW.offset); + } +} + +void TextWindow::ScreenChangeCameraNav(int link, uint32_t v) { + SS.cameraNav = !SS.cameraNav; +} + +void TextWindow::ScreenChangeImmediatelyEditDimension(int link, uint32_t v) { + SS.immediatelyEditDimension = !SS.immediatelyEditDimension; + SS.GW.Invalidate(/*clearPersistent=*/true); +} + +void TextWindow::ScreenChangeShowContourAreas(int link, uint32_t v) { + SS.showContourAreas = !SS.showContourAreas; + SS.GW.Invalidate(); +} + +void TextWindow::ScreenChangeCheckClosedContour(int link, uint32_t v) { + SS.checkClosedContour = !SS.checkClosedContour; + SS.GW.Invalidate(); +} + +void TextWindow::ScreenChangeAutomaticLineConstraints(int link, uint32_t v) { + SS.automaticLineConstraints = !SS.automaticLineConstraints; + SS.GW.Invalidate(); +} + +void TextWindow::ScreenChangeShadedTriangles(int link, uint32_t v) { + SS.exportShadedTriangles = !SS.exportShadedTriangles; + SS.GW.Invalidate(); +} + +void TextWindow::ScreenChangePwlCurves(int link, uint32_t v) { + SS.exportPwlCurves = !SS.exportPwlCurves; + SS.GW.Invalidate(); +} + +void TextWindow::ScreenChangeCanvasSizeAuto(int link, uint32_t v) { + if(link == 't') { + SS.exportCanvasSizeAuto = true; + } else { + SS.exportCanvasSizeAuto = false; + } + SS.GW.Invalidate(); +} + +void TextWindow::ScreenChangeCanvasSize(int link, uint32_t v) { + double d; + switch(v) { + case 0: d = SS.exportMargin.left; break; + case 1: d = SS.exportMargin.right; break; + case 2: d = SS.exportMargin.bottom; break; + case 3: d = SS.exportMargin.top; break; + + case 10: d = SS.exportCanvas.width; break; + case 11: d = SS.exportCanvas.height; break; + case 12: d = SS.exportCanvas.dx; break; + case 13: d = SS.exportCanvas.dy; break; + + default: return; + } + int col = 13; + if(v < 10) col = 11; + SS.TW.ShowEditControl(col, SS.MmToString(d, true)); + SS.TW.edit.meaning = Edit::CANVAS_SIZE; + SS.TW.edit.i = v; +} + +void TextWindow::ScreenChangeGCodeParameter(int link, uint32_t v) { + std::string buf; + switch(link) { + case 'd': + SS.TW.edit.meaning = Edit::G_CODE_DEPTH; + buf += SS.MmToString(SS.gCode.depth, true); + break; + + case 'h': + SS.TW.edit.meaning = Edit::G_CODE_SAFE_HEIGHT; + buf += SS.MmToString(SS.gCode.safeHeight, true); + break; + + case 's': + SS.TW.edit.meaning = Edit::G_CODE_PASSES; + buf += std::to_string(SS.gCode.passes); + break; + + case 'F': + SS.TW.edit.meaning = Edit::G_CODE_FEED; + buf += SS.MmToString(SS.gCode.feed, true); + break; + + case 'P': + SS.TW.edit.meaning = Edit::G_CODE_PLUNGE_FEED; + buf += SS.MmToString(SS.gCode.plungeFeed, true); + break; + } + SS.TW.ShowEditControl(14, buf); +} + +void TextWindow::ScreenChangeAutosaveInterval(int link, uint32_t v) { + SS.TW.ShowEditControl(3, std::to_string(SS.autosaveInterval)); + SS.TW.edit.meaning = Edit::AUTOSAVE_INTERVAL; +} + +void TextWindow::ScreenChangeFindConstraintTimeout(int link, uint32_t v) { + SS.TW.ShowEditControl(3, std::to_string(SS.timeoutRedundantConstr)); + SS.TW.edit.meaning = Edit::FIND_CONSTRAINT_TIMEOUT; +} + +void TextWindow::ScreenChangeAnimationSpeed(int link, uint32_t v) { + SS.TW.ShowEditControl(3, std::to_string(SS.animationSpeed)); + SS.TW.edit.meaning = Edit::ANIMATION_SPEED; +} + +void TextWindow::ShowConfiguration() { + int i; + Printf(true, "%Ft user color (r, g, b)"); + + for(i = 0; i < SS.MODEL_COLORS; i++) { + Printf(false, "%Bp #%d: %Bz %Bp (%@, %@, %@) %f%D%Ll%Fl[change]%E", + (i & 1) ? 'd' : 'a', + i, &SS.modelColor[i], + (i & 1) ? 'd' : 'a', + SS.modelColor[i].redF(), + SS.modelColor[i].greenF(), + SS.modelColor[i].blueF(), + &ScreenChangeColor, i); + } + + Printf(false, ""); + Printf(false, "%Ft chord tolerance (in percents)%E"); + Printf(false, "%Ba %@ %% %Fl%Ll%f%D[change]%E; %@ mm, %d triangles", + SS.chordTol, + &ScreenChangeChordTolerance, 0, SS.chordTolCalculated, + SK.GetGroup(SS.GW.activeGroup)->displayMesh.l.n); + Printf(false, "%Ft max piecewise linear segments%E"); + Printf(false, "%Ba %d %Fl%Ll%f[change]%E", + SS.maxSegments, + &ScreenChangeMaxSegments); + + Printf(false, ""); + Printf(false, "%Ft export chord tolerance (in mm)%E"); + Printf(false, "%Ba %@ %Fl%Ll%f%D[change]%E", + SS.exportChordTol, + &ScreenChangeExportChordTolerance, 0); + Printf(false, "%Ft export max piecewise linear segments%E"); + Printf(false, "%Ba %d %Fl%Ll%f[change]%E", + SS.exportMaxSegments, + &ScreenChangeExportMaxSegments); + + Printf(false, "%Ft snap grid spacing%E"); + Printf(false, "%Ba %s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.gridSpacing).c_str(), + &ScreenChangeGridSpacing, 0); + + Printf(false, ""); + Printf(false, "%Ft digits after decimal point to show%E"); + Printf(false, "%Ba%Ft distances: %Fd%d %Fl%Ll%f%D[change]%E (e.g. '%s')", + SS.UnitDigitsAfterDecimal(), + &ScreenChangeDigitsAfterDecimal, 0, + SS.MmToString(SS.StringToMm("1.23456789")).c_str()); + Printf(false, "%Bd%Ft angles: %Fd%d %Fl%Ll%f%D[change]%E (e.g. '%s')", + SS.afterDecimalDegree, + &ScreenChangeDigitsAfterDecimalDegree, 0, + SS.DegreeToString(1.23456789).c_str()); + Printf(false, " %Fd%f%Ll%s use SI prefixes for distances%E", + &ScreenChangeUseSIPrefixes, + SS.useSIPrefixes ? CHECK_TRUE : CHECK_FALSE); + + Printf(false, ""); + Printf(false, "%Ft export scale factor (1:1=mm, 1:25.4=inch)"); + Printf(false, "%Ba 1:%# %Fl%Ll%f%D[change]%E", + (double)SS.exportScale, + &ScreenChangeExportScale, 0); + Printf(false, "%Ft cutter radius offset (0=no offset) "); + Printf(false, "%Ba %s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.exportOffset).c_str(), + &ScreenChangeExportOffset, 0); + + Printf(false, ""); + Printf(false, " %Fd%f%Ll%s export shaded 2d triangles%E", + &ScreenChangeShadedTriangles, + SS.exportShadedTriangles ? CHECK_TRUE : CHECK_FALSE); + if(fabs(SS.exportOffset) > LENGTH_EPS) { + Printf(false, " %Fd%s curves as piecewise linear%E " + "(since cutter radius is not zero)", CHECK_TRUE); + } else { + Printf(false, " %Fd%f%Ll%s export curves as piecewise linear%E", + &ScreenChangePwlCurves, + SS.exportPwlCurves ? CHECK_TRUE : CHECK_FALSE); + } + Printf(false, " %Fd%f%Ll%s fix white exported lines%E", + &ScreenChangeFixExportColors, + SS.fixExportColors ? CHECK_TRUE : CHECK_FALSE); + Printf(false, " %Fd%f%Ll%s export background color%E", + &ScreenChangeExportBackgroundColor, + SS.exportBackgroundColor ? CHECK_TRUE : CHECK_FALSE); + + Printf(false, ""); + Printf(false, "%Ft export canvas size: " + "%f%Fd%Lf%s fixed%E " + "%f%Fd%Lt%s auto%E", + &ScreenChangeCanvasSizeAuto, + !SS.exportCanvasSizeAuto ? RADIO_TRUE : RADIO_FALSE, + &ScreenChangeCanvasSizeAuto, + SS.exportCanvasSizeAuto ? RADIO_TRUE : RADIO_FALSE); + + if(SS.exportCanvasSizeAuto) { + Printf(false, "%Ft (by margins around exported geometry)"); + Printf(false, "%Ba%Ft left: %Fd%s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.exportMargin.left).c_str(), &ScreenChangeCanvasSize, 0); + Printf(false, "%Bd%Ft right: %Fd%s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.exportMargin.right).c_str(), &ScreenChangeCanvasSize, 1); + Printf(false, "%Ba%Ft bottom: %Fd%s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.exportMargin.bottom).c_str(), &ScreenChangeCanvasSize, 2); + Printf(false, "%Bd%Ft top: %Fd%s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.exportMargin.top).c_str(), &ScreenChangeCanvasSize, 3); + } else { + Printf(false, "%Ft (by absolute dimensions and offsets)"); + Printf(false, "%Ba%Ft width: %Fd%s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.exportCanvas.width).c_str(), &ScreenChangeCanvasSize, 10); + Printf(false, "%Bd%Ft height: %Fd%s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.exportCanvas.height).c_str(), &ScreenChangeCanvasSize, 11); + Printf(false, "%Ba%Ft offset x: %Fd%s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.exportCanvas.dx).c_str(), &ScreenChangeCanvasSize, 12); + Printf(false, "%Bd%Ft offset y: %Fd%s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.exportCanvas.dy).c_str(), &ScreenChangeCanvasSize, 13); + } + + Printf(false, ""); + Printf(false, "%Ft exported g code parameters"); + Printf(false, "%Ba%Ft depth: %Fd%s %Fl%Ld%f[change]%E", + SS.MmToString(SS.gCode.depth).c_str(), &ScreenChangeGCodeParameter); + Printf(false, "%Bd%Ft passes: %Fd%d %Fl%Ls%f[change]%E", + SS.gCode.passes, &ScreenChangeGCodeParameter); + Printf(false, "%Ba%Ft feed: %Fd%s %Fl%LF%f[change]%E", + SS.MmToString(SS.gCode.feed).c_str(), &ScreenChangeGCodeParameter); + Printf(false, "%Bd%Ft plunge fd: %Fd%s %Fl%LP%f[change]%E", + SS.MmToString(SS.gCode.plungeFeed).c_str(), &ScreenChangeGCodeParameter); + + Printf(false, ""); + Printf(false, " %Fd%f%Ll%s draw triangle back faces in red%E", + &ScreenChangeBackFaces, + SS.drawBackFaces ? CHECK_TRUE : CHECK_FALSE); + Printf(false, " %Fd%f%Ll%s check sketch for closed contour%E", + &ScreenChangeCheckClosedContour, + SS.checkClosedContour ? CHECK_TRUE : CHECK_FALSE); + Printf(false, " %Fd%f%Ll%s show areas of closed contours%E", + &ScreenChangeShowContourAreas, + SS.showContourAreas ? CHECK_TRUE : CHECK_FALSE); + Printf(false, " %Fd%f%Ll%s enable automatic line constraints%E", + &ScreenChangeAutomaticLineConstraints, + SS.automaticLineConstraints ? CHECK_TRUE : CHECK_FALSE); + Printf(false, " %Fd%f%Ll%s use camera mouse navigation%E", &ScreenChangeCameraNav, + SS.cameraNav ? CHECK_TRUE : CHECK_FALSE); + Printf(false, " %Fd%f%Ll%s use turntable mouse navigation%E", &ScreenChangeTurntableNav, + SS.turntableNav ? CHECK_TRUE : CHECK_FALSE); + Printf(false, " %Fd%f%Ll%s edit newly added dimensions%E", + &ScreenChangeImmediatelyEditDimension, + SS.immediatelyEditDimension ? CHECK_TRUE : CHECK_FALSE); + Printf(false, " %Fd%f%Ll%s arc default is diameter%E", + &ScreenChangeArcDimDefault, + SS.arcDimDefaultDiameter ? CHECK_TRUE : CHECK_FALSE); + Printf(false, " %Fd%f%Ll%s display the full path in the title bar%E", + &ScreenChangeShowFullFilePath, SS.showFullFilePath ? CHECK_TRUE : CHECK_FALSE); + Printf(false, ""); + Printf(false, "%Ft autosave interval (in minutes)%E"); + Printf(false, "%Ba %d %Fl%Ll%f[change]%E", + SS.autosaveInterval, &ScreenChangeAutosaveInterval); + Printf(false, ""); + Printf(false, "%Ft redundant constraint timeout (in ms)%E"); + Printf(false, "%Ba %d %Fl%Ll%f[change]%E", + SS.timeoutRedundantConstr, &ScreenChangeFindConstraintTimeout); + Printf(false, ""); + Printf(false, "%Ft animation speed (in ms; 0 to disable)%E"); + Printf(false, "%Ba %d %Fl%Ll%f[change]%E", + SS.animationSpeed, &ScreenChangeAnimationSpeed); + + if(canvas) { + const char *gl_vendor, *gl_renderer, *gl_version; + canvas->GetIdent(&gl_vendor, &gl_renderer, &gl_version); + Printf(false, ""); + Printf(false, " %Ftgl vendor %E%s", gl_vendor); + Printf(false, " %Ft renderer %E%s", gl_renderer); + Printf(false, " %Ft version %E%s", gl_version); + } + + #if defined(_OPENMP) + Printf(false, " %FtOpenMP enabled"); + Printf(false, " %Ft threads %E%d", omp_get_max_threads()); + #endif +} + +bool TextWindow::EditControlDoneForConfiguration(const std::string &s) { + switch(edit.meaning) { + case Edit::LIGHT_INTENSITY: + SS.lightIntensity[edit.i] = min(1.0, max(0.0, atof(s.c_str()))); + SS.GW.Invalidate(); + break; + case Edit::LIGHT_AMBIENT: + SS.ambientIntensity = min(1.0, max(0.0, atof(s.c_str()))); + SS.GW.Invalidate(); + break; + case Edit::LIGHT_DIRECTION: { + double x, y, z; + if(sscanf(s.c_str(), "%lf, %lf, %lf", &x, &y, &z)==3) { + SS.lightDir[edit.i] = Vector::From(x, y, z); + SS.GW.Invalidate(); + } else { + Error(_("Bad format: specify coordinates as x, y, z")); + } + break; + } + case Edit::COLOR: { + Vector rgb; + if(sscanf(s.c_str(), "%lf, %lf, %lf", &rgb.x, &rgb.y, &rgb.z)==3) { + rgb = rgb.ClampWithin(0, 1); + SS.modelColor[edit.i] = RGBf(rgb.x, rgb.y, rgb.z); + } else { + Error(_("Bad format: specify color as r, g, b")); + } + break; + } + case Edit::CHORD_TOLERANCE: { + if(edit.i == 0) { + SS.chordTol = max(0.0, atof(s.c_str())); + SS.GenerateAll(SolveSpaceUI::Generate::ALL); + } else { + SS.exportChordTol = max(0.0, atof(s.c_str())); + } + break; + } + case Edit::MAX_SEGMENTS: { + if(edit.i == 0) { + SS.maxSegments = min(1000, max(7, atoi(s.c_str()))); + SS.GenerateAll(SolveSpaceUI::Generate::ALL); + } else { + SS.exportMaxSegments = min(1000, max(7, atoi(s.c_str()))); + } + break; + } + case Edit::CAMERA_TANGENT: { + SS.cameraTangent = (min(2.0, max(0.0, atof(s.c_str()))))/1000.0; + SS.GW.Invalidate(); + if(!SS.usePerspectiveProj) { + Message(_("The perspective factor will have no effect until you " + "enable View -> Use Perspective Projection.")); + } + break; + } + case Edit::GRID_SPACING: { + SS.gridSpacing = (float)min(1e4, max(1e-3, SS.StringToMm(s))); + SS.GW.Invalidate(); + break; + } + case Edit::EXPLODE_DISTANCE: { + SS.explodeDistance = min(1e4, max(-1e4, SS.StringToMm(s))); + SS.MarkGroupDirty(SS.GW.activeGroup, true); + break; + } + case Edit::DIGITS_AFTER_DECIMAL: { + int v = atoi(s.c_str()); + if(v < 0 || v > 8) { + Error(_("Specify between 0 and %d digits after the decimal."), 8); + } else { + SS.SetUnitDigitsAfterDecimal(v); + SS.GW.Invalidate(); + } + break; + } + case Edit::DIGITS_AFTER_DECIMAL_DEGREE: { + int v = atoi(s.c_str()); + if(v < 0 || v > 4) { + Error(_("Specify between 0 and %d digits after the decimal."), 4); + } else { + SS.afterDecimalDegree = v; + SS.GW.Invalidate(); + } + break; + } + case Edit::EXPORT_SCALE: { + Expr *e = Expr::From(s, /*popUpError=*/true); + if(e) { + double ev = e->Eval(); + if(fabs(ev) < 0.001 || IsReasonable(ev)) { + Error(_("Export scale must not be zero!")); + } else { + SS.exportScale = (float)ev; + } + } + break; + } + case Edit::EXPORT_OFFSET: { + Expr *e = Expr::From(s, /*popUpError=*/true); + if(e) { + double ev = SS.ExprToMm(e); + if(IsReasonable(ev) || ev < 0) { + Error(_("Cutter radius offset must not be negative!")); + } else { + SS.exportOffset = (float)ev; + } + } + break; + } + case Edit::CANVAS_SIZE: { + Expr *e = Expr::From(s, /*popUpError=*/true); + if(!e) { + break; + } + float d = (float)SS.ExprToMm(e); + switch(edit.i) { + case 0: SS.exportMargin.left = d; break; + case 1: SS.exportMargin.right = d; break; + case 2: SS.exportMargin.bottom = d; break; + case 3: SS.exportMargin.top = d; break; + + case 10: SS.exportCanvas.width = d; break; + case 11: SS.exportCanvas.height = d; break; + case 12: SS.exportCanvas.dx = d; break; + case 13: SS.exportCanvas.dy = d; break; + } + break; + } + case Edit::G_CODE_DEPTH: { + Expr *e = Expr::From(s, /*popUpError=*/true); + if(e) SS.gCode.depth = (float)SS.ExprToMm(e); + break; + } + case Edit::G_CODE_SAFE_HEIGHT: { + Expr *e = Expr::From(s, /*popUpError=*/true); + if(e) SS.gCode.safeHeight = (float)SS.ExprToMm(e); + break; + } + case Edit::G_CODE_PASSES: { + Expr *e = Expr::From(s, /*popUpError=*/true); + if(e) SS.gCode.passes = (int)(e->Eval()); + SS.gCode.passes = max(1, min(1000, SS.gCode.passes)); + break; + } + case Edit::G_CODE_FEED: { + Expr *e = Expr::From(s, /*popUpError=*/true); + if(e) SS.gCode.feed = (float)SS.ExprToMm(e); + break; + } + case Edit::G_CODE_PLUNGE_FEED: { + Expr *e = Expr::From(s, /*popUpError=*/true); + if(e) SS.gCode.plungeFeed = (float)SS.ExprToMm(e); + break; + } + case Edit::AUTOSAVE_INTERVAL: { + int interval = atoi(s.c_str()); + if(interval) { + if(interval >= 1) { + SS.autosaveInterval = interval; + SS.ScheduleAutosave(); + } else { + Error(_("Bad value: autosave interval should be positive")); + } + } else { + Error(_("Bad format: specify interval in integral minutes")); + } + break; + } + case Edit::FIND_CONSTRAINT_TIMEOUT: { + int timeout = atoi(s.c_str()); + if(timeout) { + if(timeout >= 1) { + SS.timeoutRedundantConstr = timeout; + } else { + SS.timeoutRedundantConstr = 1000; + } + } + break; + } + case Edit::ANIMATION_SPEED: { + int speed = atoi(s.c_str()); + if(speed >= 0) { + SS.animationSpeed = speed; + } else { + SS.animationSpeed = 800; + } + break; + } + + default: return false; + } + return true; +} + +} // namespace SolveSpace diff --git a/src/constraint.cpp b/src/constraint.cpp new file mode 100644 index 0000000..3075d31 --- /dev/null +++ b/src/constraint.cpp @@ -0,0 +1,994 @@ +//----------------------------------------------------------------------------- +// Implementation of the Constraint menu, to create new constraints in +// the sketch. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +std::string Constraint::DescriptionString() const { + std::string s; + switch(type) { + case Type::POINTS_COINCIDENT: s = C_("constr-name", "pts-coincident"); break; + case Type::PT_PT_DISTANCE: s = C_("constr-name", "pt-pt-distance"); break; + case Type::PT_LINE_DISTANCE: s = C_("constr-name", "pt-line-distance"); break; + case Type::PT_PLANE_DISTANCE: s = C_("constr-name", "pt-plane-distance"); break; + case Type::PT_FACE_DISTANCE: s = C_("constr-name", "pt-face-distance"); break; + case Type::PROJ_PT_DISTANCE: s = C_("constr-name", "proj-pt-pt-distance"); break; + case Type::PT_IN_PLANE: s = C_("constr-name", "pt-in-plane"); break; + case Type::PT_ON_LINE: s = C_("constr-name", "pt-on-line"); break; + case Type::PT_ON_FACE: s = C_("constr-name", "pt-on-face"); break; + case Type::EQUAL_LENGTH_LINES: s = C_("constr-name", "eq-length"); break; + case Type::EQ_LEN_PT_LINE_D: s = C_("constr-name", "eq-length-and-pt-ln-dist"); break; + case Type::EQ_PT_LN_DISTANCES: s = C_("constr-name", "eq-pt-line-distances"); break; + case Type::LENGTH_RATIO: s = C_("constr-name", "length-ratio"); break; + case Type::ARC_ARC_LEN_RATIO: s = C_("constr-name", "arc-arc-length-ratio"); break; + case Type::ARC_LINE_LEN_RATIO: s = C_("constr-name", "arc-line-length-ratio"); break; + case Type::LENGTH_DIFFERENCE: s = C_("constr-name", "length-difference"); break; + case Type::ARC_ARC_DIFFERENCE: s = C_("constr-name", "arc-arc-len-difference"); break; + case Type::ARC_LINE_DIFFERENCE: s = C_("constr-name", "arc-line-len-difference"); break; + case Type::SYMMETRIC: s = C_("constr-name", "symmetric"); break; + case Type::SYMMETRIC_HORIZ: s = C_("constr-name", "symmetric-h"); break; + case Type::SYMMETRIC_VERT: s = C_("constr-name", "symmetric-v"); break; + case Type::SYMMETRIC_LINE: s = C_("constr-name", "symmetric-line"); break; + case Type::AT_MIDPOINT: s = C_("constr-name", "at-midpoint"); break; + case Type::HORIZONTAL: s = C_("constr-name", "horizontal"); break; + case Type::VERTICAL: s = C_("constr-name", "vertical"); break; + case Type::DIAMETER: s = C_("constr-name", "diameter"); break; + case Type::PT_ON_CIRCLE: s = C_("constr-name", "pt-on-circle"); break; + case Type::SAME_ORIENTATION: s = C_("constr-name", "same-orientation"); break; + case Type::ANGLE: s = C_("constr-name", "angle"); break; + case Type::PARALLEL: s = C_("constr-name", "parallel"); break; + case Type::ARC_LINE_TANGENT: s = C_("constr-name", "arc-line-tangent"); break; + case Type::CUBIC_LINE_TANGENT: s = C_("constr-name", "cubic-line-tangent"); break; + case Type::CURVE_CURVE_TANGENT: s = C_("constr-name", "curve-curve-tangent"); break; + case Type::PERPENDICULAR: s = C_("constr-name", "perpendicular"); break; + case Type::EQUAL_RADIUS: s = C_("constr-name", "eq-radius"); break; + case Type::EQUAL_ANGLE: s = C_("constr-name", "eq-angle"); break; + case Type::EQUAL_LINE_ARC_LEN: s = C_("constr-name", "eq-line-len-arc-len"); break; + case Type::WHERE_DRAGGED: s = C_("constr-name", "lock-where-dragged"); break; + case Type::COMMENT: s = C_("constr-name", "comment"); break; + default: s = "???"; break; + } + + return ssprintf("c%03x-%s", h.v, s.c_str()); +} + +#ifndef LIBRARY + +//----------------------------------------------------------------------------- +// Delete all constraints with the specified type, entityA, ptA. We use this +// when auto-removing constraints that would become redundant. +//----------------------------------------------------------------------------- +void Constraint::DeleteAllConstraintsFor(Constraint::Type type, hEntity entityA, hEntity ptA) +{ + SK.constraint.ClearTags(); + for(auto &constraint : SK.constraint) { + ConstraintBase *ct = &constraint; + if(ct->type != type) continue; + + if(ct->entityA != entityA) continue; + if(ct->ptA != ptA) continue; + ct->tag = 1; + } + SK.constraint.RemoveTagged(); + // And no need to do anything special, since nothing + // ever depends on a constraint. But do clear the + // hover, in case the just-deleted constraint was + // hovered. + SS.GW.hover.Clear(); +} + +hConstraint Constraint::AddConstraint(Constraint *c, bool rememberForUndo) { + if(rememberForUndo) SS.UndoRemember(); + + hConstraint hc = SK.constraint.AddAndAssignId(c); + SK.GetConstraint(hc)->Generate(&SK.param); + + SS.MarkGroupDirty(c->group); + SK.GetGroup(c->group)->dofCheckOk = false; + return c->h; +} + +hConstraint Constraint::Constrain(Constraint::Type type, hEntity ptA, hEntity ptB, + hEntity entityA, hEntity entityB, + bool other, bool other2) +{ + Constraint c = {}; + c.group = SS.GW.activeGroup; + c.workplane = SS.GW.ActiveWorkplane(); + c.type = type; + c.ptA = ptA; + c.ptB = ptB; + c.entityA = entityA; + c.entityB = entityB; + c.other = other; + c.other2 = other2; + return AddConstraint(&c, /*rememberForUndo=*/false); +} + +hConstraint Constraint::TryConstrain(Constraint::Type type, hEntity ptA, hEntity ptB, + hEntity entityA, hEntity entityB, + bool other, bool other2) { + int rankBefore, rankAfter; + SolveResult howBefore = SS.TestRankForGroup(SS.GW.activeGroup, &rankBefore); + hConstraint hc = Constrain(type, ptA, ptB, entityA, entityB, other, other2); + SolveResult howAfter = SS.TestRankForGroup(SS.GW.activeGroup, &rankAfter); + // There are two cases where the constraint is clearly redundant: + // * If the group wasn't overconstrained and now it is; + // * If the group was overconstrained, and adding the constraint doesn't change rank at all. + if((howBefore == SolveResult::OKAY && howAfter == SolveResult::REDUNDANT_OKAY) || + (howBefore == SolveResult::REDUNDANT_OKAY && howAfter == SolveResult::REDUNDANT_OKAY && + rankBefore == rankAfter)) { + SK.constraint.RemoveById(hc); + hc = {}; + } + return hc; +} + +hConstraint Constraint::ConstrainCoincident(hEntity ptA, hEntity ptB) { + return Constrain(Type::POINTS_COINCIDENT, ptA, ptB, + Entity::NO_ENTITY, Entity::NO_ENTITY, /*other=*/false, /*other2=*/false); +} + +bool Constraint::ConstrainArcLineTangent(Constraint *c, Entity *line, Entity *arc, + Entity *arcendpoint) { + Vector l0 = SK.GetEntity(line->point[0])->PointGetNum(), + l1 = SK.GetEntity(line->point[1])->PointGetNum(); + Vector a1 = SK.GetEntity(arc->point[1])->PointGetNum(), + a2 = SK.GetEntity(arc->point[2])->PointGetNum(); + if(l0.Equals(a1) || l1.Equals(a1)) { + c->other = false; + } else if(l0.Equals(a2) || l1.Equals(a2)) { + c->other = true; + } else if(nullptr != arcendpoint) { + Vector p = arcendpoint->PointGetNum(); + if(a1.Equals(p)) { + c->other = false; + } else if(a2.Equals(p)) { + c->other = true; + } else { + Error(_("The point you selected does not belong to the arc. " + "The arc and line segment do not share an end point.\n\n" + "Select the end point of the arc at which you want " + "it to be tangent to the line.")); + return false; + } + } else { + Error(_("The tangent arc and line segment must share an " + "endpoint. Constrain them with Constrain -> " + "On Point before constraining tangent.\n\n" + "Alternatively select the end point of the arc " + "at which you want it to be tangent to the line.")); + return false; + } + return true; +} + +bool Constraint::ConstrainCubicLineTangent(Constraint *c, Entity *line, Entity *cubic, + Entity *curveendpoint) { + Vector l0 = SK.GetEntity(line->point[0])->PointGetNum(), + l1 = SK.GetEntity(line->point[1])->PointGetNum(); + Vector as = cubic->CubicGetStartNum(), + af = cubic->CubicGetFinishNum(); + + if(l0.Equals(as) || l1.Equals(as)) { + c->other = false; + } else if(l0.Equals(af) || l1.Equals(af)) { + c->other = true; + } else if(nullptr != curveendpoint) { + Vector p = curveendpoint->PointGetNum(); + if(as.Equals(p)) { + c->other = false; + } else if(af.Equals(p)) { + c->other = true; + } else { + Error(_("The point you selected is not an end point of the cubic spline. " + "The spline and line segment do not share an end point.\n\n" + "Select the end point of the spline at which you want " + "it to be tangent to the line.")); + return false; + } + } else { + Error(_("The tangent cubic spline and line segment must share an " + "endpoint. Constrain them with Constrain -> " + "On Point before constraining tangent.\n\n" + "Alternatively select the end point of the cubic spline " + "at which you want it to be tangent to the line.")); + return false; + } + return true; +} + +bool Constraint::ConstrainCurveCurveTangent(Constraint *c, Entity *eA, Entity *eB, Entity *epA, + Entity *epB) { + Vector as = eA->EndpointStart(), + af = eA->EndpointFinish(), + bs = eB->EndpointStart(), + bf = eB->EndpointFinish(); + if(as.Equals(bs)) { + c->other = false; + c->other2 = false; + } else if(as.Equals(bf)) { + c->other = false; + c->other2 = true; + } else if(af.Equals(bs)) { + c->other = true; + c->other2 = false; + } else if(af.Equals(bf)) { + c->other = true; + c->other2 = true; + } else if((nullptr != epA) && (nullptr != epB)) { + Vector pa = epA->PointGetNum(), + pb = epB->PointGetNum(); + if((as.Equals(pa) && bs.Equals(pb)) || (as.Equals(pb) && bs.Equals(pa))) { + c->other = false; + c->other2 = false; + } else if((as.Equals(pa) && bf.Equals(pb)) || (as.Equals(pb) && bf.Equals(pa))) { + c->other = false; + c->other2 = true; + } else if((af.Equals(pa) && bs.Equals(pb)) || (af.Equals(pb) && bs.Equals(pa))) { + c->other = true; + c->other2 = false; + } else if((af.Equals(pa) && bf.Equals(pb)) || (af.Equals(pb) && bf.Equals(pa))) { + c->other = true; + c->other2 = true; + } else { + Error(_("The points you selected are not end points of the two curves. " + "The curves do not share an end point.\n\n" + "Select the end points of both curves at which you want " + "them to be tangent to each other.")); + return false; + } + } else { + Error(_("The curves must share an endpoint. Constrain them " + "with Constrain -> On Point before constraining " + "tangent.\n\n" + "Alternatively select the end points of both " + "curves at which you want the curves to be tangent.")); + return false; + } + return true; +} + +void Constraint::MenuConstrain(Command id) { + std::vector newcons; + + Constraint c = {}; + c.group = SS.GW.activeGroup; + c.workplane = SS.GW.ActiveWorkplane(); + + SS.GW.GroupSelection(); + auto const &gs = SS.GW.gs; + + switch(id) { + case Command::DISTANCE_DIA: + case Command::REF_DISTANCE: { + if(gs.points == 2 && gs.n == 2) { + c.type = Type::PT_PT_DISTANCE; + c.ptA = gs.point[0]; + c.ptB = gs.point[1]; + } else if(gs.lineSegments == 1 && gs.n == 1) { + c.type = Type::PT_PT_DISTANCE; + Entity *e = SK.GetEntity(gs.entity[0]); + c.ptA = e->point[0]; + c.ptB = e->point[1]; + } else if(gs.vectors == 1 && gs.points == 2 && gs.n == 3) { + c.type = Type::PROJ_PT_DISTANCE; + c.ptA = gs.point[0]; + c.ptB = gs.point[1]; + c.entityA = gs.vector[0]; + } else if(gs.workplanes == 1 && gs.points == 1 && gs.n == 2) { + c.type = Type::PT_PLANE_DISTANCE; + c.ptA = gs.point[0]; + c.entityA = gs.entity[0]; + } else if(gs.lineSegments == 1 && gs.points == 1 && gs.n == 2) { + c.type = Type::PT_LINE_DISTANCE; + c.ptA = gs.point[0]; + c.entityA = gs.entity[0]; + } else if(gs.faces == 1 && gs.points == 1 && gs.n == 2) { + c.type = Type::PT_FACE_DISTANCE; + c.ptA = gs.point[0]; + c.entityA = gs.face[0]; + } else if(gs.circlesOrArcs == 1 && gs.n == 1) { + c.type = Type::DIAMETER; + c.entityA = gs.entity[0]; + Entity* arc = SK.GetEntity(gs.entity[0]); + if ((arc->type == EntityBase::Type::ARC_OF_CIRCLE) + && (!SS.arcDimDefaultDiameter)) + { + c.other = true; + } + } else { + Error(_("Bad selection for distance / diameter constraint. This " + "constraint can apply to:\n\n" + " * two points (distance between points)\n" + " * a line segment (length)\n" + " * two points and a line segment or normal (projected distance)\n" + " * a workplane and a point (minimum distance)\n" + " * a line segment and a point (minimum distance)\n" + " * a plane face and a point (minimum distance)\n" + " * a circle or an arc (diameter)\n")); + return; + } + if(c.type == Type::PT_PT_DISTANCE || c.type == Type::PROJ_PT_DISTANCE) { + Vector n = SS.GW.projRight.Cross(SS.GW.projUp); + Vector a = SK.GetEntity(c.ptA)->PointGetNum(); + Vector b = SK.GetEntity(c.ptB)->PointGetNum(); + c.disp.offset = n.Cross(a.Minus(b)); + c.disp.offset = (c.disp.offset).WithMagnitude(50/SS.GW.scale); + } else { + c.disp.offset = Vector::From(0, 0, 0); + } + + if(id == Command::REF_DISTANCE) { + c.reference = true; + } + + c.valA = 0; + c.ModifyToSatisfy(); + AddConstraint(&c); + newcons.push_back(c); + break; + } + + case Command::ON_ENTITY: + if(gs.points >= 2 && gs.points == gs.n) { + c.type = Type::POINTS_COINCIDENT; + c.ptA = gs.point[0]; + for(int k = 1; k < gs.points; k++) { + c.ptB = gs.point[k]; + newcons.push_back(c); + } + } else if(gs.points == 1 && gs.workplanes == 1 && gs.n == 2) { + c.type = Type::PT_IN_PLANE; + c.ptA = gs.point[0]; + c.entityA = gs.entity[0]; + newcons.push_back(c); + } else if(gs.points == 1 && gs.lineSegments == 1 && gs.n == 2) { + c.type = Type::PT_ON_LINE; + c.ptA = gs.point[0]; + c.entityA = gs.entity[0]; + newcons.push_back(c); + } else if(gs.points == 1 && gs.circlesOrArcs == 1 && gs.n == 2) { + c.type = Type::PT_ON_CIRCLE; + c.ptA = gs.point[0]; + c.entityA = gs.entity[0]; + newcons.push_back(c); + } else if(gs.points == 1 && gs.faces >= 1 && gs.n == gs.points+gs.faces) { + c.type = Type::PT_ON_FACE; + c.ptA = gs.point[0]; + for (int k=0; k= 2 && gs.lineSegments == gs.n) { + c.type = Type::EQUAL_LENGTH_LINES; + c.entityA = gs.entity[0]; + for (std::vector::size_type k = 1;k < gs.entity.size(); ++k){ + c.entityB = gs.entity[k]; + newcons.push_back(c); + } + } else if(gs.lineSegments == 2 && gs.points == 2 && gs.n == 4) { + c.type = Type::EQ_PT_LN_DISTANCES; + c.entityA = gs.entity[0]; + c.ptA = gs.point[0]; + c.entityB = gs.entity[1]; + c.ptB = gs.point[1]; + newcons.push_back(c); + } else if(gs.lineSegments == 1 && gs.points == 2 && gs.n == 3) { + // The same line segment for the distances, but different + // points. + c.type = Type::EQ_PT_LN_DISTANCES; + c.entityA = gs.entity[0]; + c.ptA = gs.point[0]; + c.entityB = gs.entity[0]; + c.ptB = gs.point[1]; + newcons.push_back(c); + } else if(gs.lineSegments == 2 && gs.points == 1 && gs.n == 3) { + c.type = Type::EQ_LEN_PT_LINE_D; + c.entityA = gs.entity[0]; + c.entityB = gs.entity[1]; + c.ptA = gs.point[0]; + newcons.push_back(c); + } else if(gs.circlesOrArcs >= 2 && gs.circlesOrArcs == gs.n) { + c.type = Type::EQUAL_RADIUS; + c.entityA = gs.entity[0]; + for (std::vector::size_type k = 1;k < gs.entity.size(); ++k){ + c.entityB = gs.entity[k]; + newcons.push_back(c); + } + } else if(gs.arcs == 1 && gs.lineSegments == 1 && gs.n == 2) { + c.type = Type::EQUAL_LINE_ARC_LEN; + if(SK.GetEntity(gs.entity[0])->type == Entity::Type::ARC_OF_CIRCLE) { + c.entityA = gs.entity[1]; + c.entityB = gs.entity[0]; + } else { + c.entityA = gs.entity[0]; + c.entityB = gs.entity[1]; + } + newcons.push_back(c); + } else { + Error(_("Bad selection for equal length / radius constraint. " + "This constraint can apply to:\n\n" + " * two or more line segments (equal length)\n" + " * two line segments and two points " + "(equal point-line distances)\n" + " * a line segment and two points " + "(equal point-line distances)\n" + " * a line segment, and a point and line segment " + "(point-line distance equals length)\n" + " * two or more circles or arcs (equal radius)\n" + " * a line segment and an arc " + "(line segment length equals arc length)\n")); + return; + } + SS.UndoRemember(); + for (auto&& nc : newcons){ + if(nc.type == Type::EQUAL_ANGLE) { + // Infer the nearest supplementary angle from the sketch. + Vector a1 = SK.GetEntity(c.entityA)->VectorGetNum(), + b1 = SK.GetEntity(c.entityB)->VectorGetNum(), + a2 = SK.GetEntity(c.entityC)->VectorGetNum(), + b2 = SK.GetEntity(c.entityD)->VectorGetNum(); + double d1 = a1.Dot(b1), d2 = a2.Dot(b2); + + if(d1*d2 < 0) { + nc.other = true; + } + } + AddConstraint(&nc, /*rememberForUndo=*/false); + } + break; + + case Command::RATIO: + if(gs.lineSegments == 2 && gs.n == 2) { + c.type = Type::LENGTH_RATIO; + c.entityA = gs.entity[0]; + c.entityB = gs.entity[1]; + } + else if(gs.arcs == 2 && gs.n == 2) { + c.type = Type::ARC_ARC_LEN_RATIO; + c.entityA = gs.entity[0]; + c.entityB = gs.entity[1]; + } + else if(gs.lineSegments == 1 && gs.arcs == 1 && gs.n == 2) { + c.type = Type::ARC_LINE_LEN_RATIO; + if(SK.GetEntity(gs.entity[0])->type == Entity::Type::ARC_OF_CIRCLE) { + c.entityA = gs.entity[1]; + c.entityB = gs.entity[0]; + } else { + c.entityA = gs.entity[0]; + c.entityB = gs.entity[1]; + } + } else { + Error(_("Bad selection for length ratio constraint. This " + "constraint can apply to:\n\n" + " * two line segments\n" + " * two arcs\n" + " * one arc and one line segment\n")); + return; + } + + c.valA = 0; + c.ModifyToSatisfy(); + AddConstraint(&c); + newcons.push_back(c); + break; + + case Command::DIFFERENCE: + if(gs.lineSegments == 2 && gs.n == 2) { + c.type = Type::LENGTH_DIFFERENCE; + c.entityA = gs.entity[0]; + c.entityB = gs.entity[1]; + } + else if(gs.arcs == 2 && gs.n == 2) { + c.type = Type::ARC_ARC_DIFFERENCE; + c.entityA = gs.entity[0]; + c.entityB = gs.entity[1]; + } + else if(gs.lineSegments == 1 && gs.arcs == 1 && gs.n == 2) { + c.type = Type::ARC_LINE_DIFFERENCE; + if(SK.GetEntity(gs.entity[0])->type == Entity::Type::ARC_OF_CIRCLE) { + c.entityA = gs.entity[1]; + c.entityB = gs.entity[0]; + } else { + c.entityA = gs.entity[0]; + c.entityB = gs.entity[1]; + } + } else { + Error(_("Bad selection for length difference constraint. This " + "constraint can apply to:\n\n" + " * two line segments\n" + " * two arcs\n" + " * one arc and one line segment\n")); + return; + } + + c.valA = 0; + c.ModifyToSatisfy(); + AddConstraint(&c); + newcons.push_back(c); + break; + + case Command::AT_MIDPOINT: + if(gs.lineSegments == 1 && gs.points == 1 && gs.n == 2) { + c.type = Type::AT_MIDPOINT; + c.entityA = gs.entity[0]; + c.ptA = gs.point[0]; + + // If a point is at-midpoint, then no reason to also constrain + // it on-line; so auto-remove that. Handle as one undo group. + SS.UndoRemember(); + DeleteAllConstraintsFor(Type::PT_ON_LINE, c.entityA, c.ptA); + AddConstraint(&c, /*rememberForUndo=*/false); + newcons.push_back(c); + break; + } else if(gs.lineSegments == 1 && gs.workplanes == 1 && gs.n == 2) { + c.type = Type::AT_MIDPOINT; + int i = SK.GetEntity(gs.entity[0])->IsWorkplane() ? 1 : 0; + c.entityA = gs.entity[i]; + c.entityB = gs.entity[1-i]; + AddConstraint(&c); + newcons.push_back(c); + } else { + Error(_("Bad selection for at midpoint constraint. This " + "constraint can apply to:\n\n" + " * a line segment and a point " + "(point at midpoint)\n" + " * a line segment and a workplane " + "(line's midpoint on plane)\n")); + return; + } + + break; + + case Command::SYMMETRIC: + if(gs.points == 2 && + ((gs.workplanes == 1 && gs.n == 3) || + (gs.n == 2))) + { + if(gs.entities > 0) + c.entityA = gs.entity[0]; + c.ptA = gs.point[0]; + c.ptB = gs.point[1]; + c.type = Type::SYMMETRIC; + } else if(gs.lineSegments == 1 && + ((gs.workplanes == 1 && gs.n == 2) || + (gs.n == 1))) + { + Entity *line; + if(SK.GetEntity(gs.entity[0])->IsWorkplane()) { + line = SK.GetEntity(gs.entity[1]); + c.entityA = gs.entity[0]; + } else { + line = SK.GetEntity(gs.entity[0]); + } + c.ptA = line->point[0]; + c.ptB = line->point[1]; + c.type = Type::SYMMETRIC; + } else if(SS.GW.LockedInWorkplane() + && gs.lineSegments == 2 && gs.n == 2) + { + Entity *l0 = SK.GetEntity(gs.entity[0]), + *l1 = SK.GetEntity(gs.entity[1]); + + if((l1->group != SS.GW.activeGroup) || + (l1->construction && !(l0->construction))) + { + swap(l0, l1); + } + c.ptA = l1->point[0]; + c.ptB = l1->point[1]; + c.entityA = l0->h; + c.type = Type::SYMMETRIC_LINE; + } else if(SS.GW.LockedInWorkplane() + && gs.lineSegments == 1 && gs.points == 2 && gs.n == 3) + { + c.ptA = gs.point[0]; + c.ptB = gs.point[1]; + c.entityA = gs.entity[0]; + c.type = Type::SYMMETRIC_LINE; + } else { + Error(_("Bad selection for symmetric constraint. This constraint " + "can apply to:\n\n" + " * two points or a line segment " + "(symmetric about workplane's coordinate axis)\n" + " * line segment, and two points or a line segment " + "(symmetric about line segment)\n" + " * workplane, and two points or a line segment " + "(symmetric about workplane)\n")); + return; + } + // We may remove constraints so remember manually + if(c.entityA == Entity::NO_ENTITY) { + // Horizontal / vertical symmetry, implicit symmetry plane + // normal to the workplane + if(c.workplane == Entity::FREE_IN_3D) { + Error(_("A workplane must be active when constraining " + "symmetric without an explicit symmetry plane.")); + return; + } + Vector pa = SK.GetEntity(c.ptA)->PointGetNum(); + Vector pb = SK.GetEntity(c.ptB)->PointGetNum(); + Vector dp = pa.Minus(pb); + EntityBase *norm = SK.GetEntity(c.workplane)->Normal();; + Vector u = norm->NormalU(), v = norm->NormalV(); + if(fabs(dp.Dot(u)) > fabs(dp.Dot(v))) { + c.type = Type::SYMMETRIC_HORIZ; + } else { + c.type = Type::SYMMETRIC_VERT; + } + if(gs.lineSegments == 1) { + // If this line segment is already constrained horiz or + // vert, then auto-remove that redundant constraint. + // Handle as one undo group. + SS.UndoRemember(); + DeleteAllConstraintsFor(Type::HORIZONTAL, (gs.entity[0]), + Entity::NO_ENTITY); + DeleteAllConstraintsFor(Type::VERTICAL, (gs.entity[0]), + Entity::NO_ENTITY); + AddConstraint(&c, /*rememberForUndo=*/false); + newcons.push_back(c); + break; + } + } + AddConstraint(&c); + newcons.push_back(c); + break; + + case Command::VERTICAL: + case Command::HORIZONTAL: { + if(id == Command::HORIZONTAL) { + c.type = Type::HORIZONTAL; + } else { + c.type = Type::VERTICAL; + } + if(c.workplane == Entity::FREE_IN_3D) { + Error(_("Activate a workplane (with Sketch -> In Workplane) before " + "applying a horizontal or vertical constraint.")); + return; + } + if(gs.lineSegments > 0 && gs.lineSegments == gs.n) { + for (auto enti : gs.entity){ + c.entityA = enti; + newcons.push_back(c); + } + } else if(gs.points >= 2 && gs.n == gs.points) { + c.ptA = gs.point[0]; + for (int k = 1; kgroup == SS.GW.activeGroup) { + swap(nref, nfree); + } + if(nfree->group == SS.GW.activeGroup && nref->group != SS.GW.activeGroup) { + // nfree is free, and nref is locked (since it came from a + // previous group); so let's force nfree aligned to nref, + // and make convergence easy + Vector ru = nref ->NormalU(), rv = nref ->NormalV(); + Vector fu = nfree->NormalU(), fv = nfree->NormalV(); + + if(fabs(fu.Dot(ru)) < fabs(fu.Dot(rv))) { + // There might be an odd*90 degree rotation about the + // normal vector; allow that, since the numerical + // constraint does + swap(ru, rv); + } + fu = fu.Dot(ru) > 0 ? ru : ru.ScaledBy(-1); + fv = fv.Dot(rv) > 0 ? rv : rv.ScaledBy(-1); + + nfree->NormalForceTo(Quaternion::From(fu, fv)); + } + AddConstraint(&c, /*rememberForUndo=*/false); + newcons.push_back(c); + break; + } + + case Command::OTHER_ANGLE: + if(gs.constraints == 1 && gs.n == 0) { + Constraint *c = SK.GetConstraint(gs.constraint[0]); + if(c->type == Type::ANGLE) { + SS.UndoRemember(); + c->other = !(c->other); + c->ModifyToSatisfy(); + break; + } + if(c->type == Type::EQUAL_ANGLE) { + SS.UndoRemember(); + c->other = !(c->other); + SS.MarkGroupDirty(c->group); + break; + } + } + Error(_("Must select an angle constraint.")); + return; + + case Command::REFERENCE: + if(gs.constraints == 1 && gs.n == 0) { + Constraint *c = SK.GetConstraint(gs.constraint[0]); + if(c->HasLabel() && c->type != Type::COMMENT) { + SS.UndoRemember(); + (c->reference) = !(c->reference); + SS.MarkGroupDirty(c->group, /*onlyThis=*/true); + break; + } + } + Error(_("Must select a constraint with associated label.")); + return; + + case Command::ANGLE: + case Command::REF_ANGLE: { + if(gs.vectors == 3 && gs.n == 3) { + c.type = Type::EQUAL_ANGLE; + c.entityA = gs.vector[0]; + c.entityB = gs.vector[1]; + c.entityC = gs.vector[1]; + c.entityD = gs.vector[2]; + } else if(gs.vectors == 4 && gs.n == 4) { + c.type = Type::EQUAL_ANGLE; + c.entityA = gs.vector[0]; + c.entityB = gs.vector[1]; + c.entityC = gs.vector[2]; + c.entityD = gs.vector[3]; + } else if(gs.vectors == 2 && gs.n == 2) { + c.type = Type::ANGLE; + c.entityA = gs.vector[0]; + c.entityB = gs.vector[1]; + c.valA = 0; + } else { + Error(_("Bad selection for angle constraint. This constraint " + "can apply to:\n\n" + "Angle between:\n" + " * two line segments\n" + " * a line segment and a normal\n" + " * two normals\n" + "\nEqual angles:\n" + " * four line segments or normals " + "(equal angle between A,B and C,D)\n" + " * three line segments or normals " + "(equal angle between A,B and B,C)\n")); + return; + } + + Entity *ea = SK.GetEntity(c.entityA), + *eb = SK.GetEntity(c.entityB); + if(ea->type == Entity::Type::LINE_SEGMENT && + eb->type == Entity::Type::LINE_SEGMENT) + { + Vector a0 = SK.GetEntity(ea->point[0])->PointGetNum(), + a1 = SK.GetEntity(ea->point[1])->PointGetNum(), + b0 = SK.GetEntity(eb->point[0])->PointGetNum(), + b1 = SK.GetEntity(eb->point[1])->PointGetNum(); + if(a0.Equals(b0) || a1.Equals(b1)) { + // okay, vectors should be drawn in same sense + } else if(a0.Equals(b1) || a1.Equals(b0)) { + // vectors are in opposite sense + c.other = true; + } else { + // no shared point; not clear which intersection to draw + } + } + + if(id == Command::REF_ANGLE) { + c.reference = true; + } + + c.ModifyToSatisfy(); + AddConstraint(&c); + newcons.push_back(c); + break; + } + + case Command::PARALLEL: + if(gs.faces == 2 && gs.n == 2) { + c.type = Type::PARALLEL; + c.entityA = gs.face[0]; + c.entityB = gs.face[1]; + newcons.push_back(c); + } else if(gs.vectors > 1 && gs.vectors == gs.n) { + c.type = Type::PARALLEL; + c.entityA = gs.vector[0]; + for (std::vector::size_type k = 1; k < gs.vector.size();++k ){ + c.entityB = gs.vector[k]; + newcons.push_back(c); + } + } else if(gs.lineSegments == 1 && gs.arcs == 1 && + (gs.n == 2 || (gs.points == 1 && gs.n == 3))) { + Entity *line = SK.GetEntity(gs.entity[0]), + *arc = SK.GetEntity(gs.entity[1]); + if(line->type == Entity::Type::ARC_OF_CIRCLE) { + swap(line, arc); + } + if(!ConstrainArcLineTangent( + &c, line, arc, (1 == gs.points) ? SK.GetEntity(gs.point[0]) : nullptr)) { + return; + } + c.type = Type::ARC_LINE_TANGENT; + c.entityA = arc->h; + c.entityB = line->h; + newcons.push_back(c); + } else if(gs.lineSegments == 1 && gs.cubics == 1 && + (gs.n == 2 || (gs.points == 1 && gs.n == 3))) { + Entity *line = SK.GetEntity(gs.entity[0]), + *cubic = SK.GetEntity(gs.entity[1]); + if(line->type == Entity::Type::CUBIC) { + swap(line, cubic); + } + if(!ConstrainCubicLineTangent( + &c, line, cubic, (1 == gs.points) ? SK.GetEntity(gs.point[0]) : nullptr)) { + return; + } + c.type = Type::CUBIC_LINE_TANGENT; + c.entityA = cubic->h; + c.entityB = line->h; + newcons.push_back(c); + } else if(gs.cubics + gs.arcs == 2 && (gs.n == 2 || (gs.points == 2 && gs.n == 4))) { + if(!SS.GW.LockedInWorkplane()) { + Error(_("Curve-curve tangency must apply in workplane.")); + return; + } + Entity *eA = SK.GetEntity(gs.entity[0]), + *eB = SK.GetEntity(gs.entity[1]); + if(!ConstrainCurveCurveTangent( + &c, eA, eB, (2 == gs.points) ? SK.GetEntity(gs.point[0]) : nullptr, + (2 == gs.points) ? SK.GetEntity(gs.point[1]) : nullptr)) { + return; + } + c.type = Type::CURVE_CURVE_TANGENT; + c.entityA = eA->h; + c.entityB = eB->h; + newcons.push_back(c); + } else { + Error(_("Bad selection for parallel / tangent constraint. This " + "constraint can apply to:\n\n" + " * two faces\n" + " * two or more line segments (parallel)\n" + " * one or more line segments and one or more normals (parallel)\n" + " * two or more normals (parallel)\n" + " * two line segments, arcs, or beziers, that share " + "an endpoint (tangent)\n" + " * two line segments, arcs, or beziers, that do not share " + "an endpoint and the end point(s) of the curve(s) (tangent)\n")); + return; + } + SS.UndoRemember(); + for (auto&& nc:newcons) + AddConstraint(&nc, /*rememberForUndo=*/false); + break; + + case Command::PERPENDICULAR: + if(gs.faces == 2 && gs.n == 2) { + c.type = Type::PERPENDICULAR; + c.entityA = gs.face[0]; + c.entityB = gs.face[1]; + } else if(gs.vectors == 2 && gs.n == 2) { + c.type = Type::PERPENDICULAR; + c.entityA = gs.vector[0]; + c.entityB = gs.vector[1]; + } else { + Error(_("Bad selection for perpendicular constraint. This " + "constraint can apply to:\n\n" + " * two faces\n" + " * two line segments\n" + " * a line segment and a normal\n" + " * two normals\n")); + return; + } + AddConstraint(&c); + newcons.push_back(c); + break; + + case Command::WHERE_DRAGGED: + if(gs.points == 1 && gs.n == 1) { + c.type = Type::WHERE_DRAGGED; + c.ptA = gs.point[0]; + } else { + Error(_("Bad selection for lock point where dragged constraint. " + "This constraint can apply to:\n\n" + " * a point\n")); + return; + } + AddConstraint(&c); + newcons.push_back(c); + break; + + case Command::COMMENT: + if(gs.points == 1 && gs.n == 1) { + c.type = Type::COMMENT; + c.ptA = gs.point[0]; + c.group = SS.GW.activeGroup; + c.workplane = SS.GW.ActiveWorkplane(); + c.comment = _("NEW COMMENT -- DOUBLE-CLICK TO EDIT"); + AddConstraint(&c); + newcons.push_back(c); + } else { + SS.GW.pending.operation = GraphicsWindow::Pending::COMMAND; + SS.GW.pending.command = Command::COMMENT; + SS.GW.pending.description = _("click center of comment text"); + SS.ScheduleShowTW(); + } + break; + + default: ssassert(false, "Unexpected menu ID"); + } + for (auto nc:newcons){ + for(const Constraint &cc : SK.constraint) { + if(nc.h != cc.h && nc.Equals(cc)) { + // Oops, we already have this exact constraint. Remove the one we just added. + SK.constraint.RemoveById(nc.h); + SS.GW.ClearSelection(); + // And now select the old one, to give feedback. + SS.GW.MakeSelected(cc.h); + return; + } + } + + if(SK.constraint.FindByIdNoOops(nc.h)) { + Constraint *constraint = SK.GetConstraint(nc.h); + if(SS.TestRankForGroup(nc.group) == SolveResult::REDUNDANT_OKAY && + !SK.GetGroup(SS.GW.activeGroup)->allowRedundant && + constraint->HasLabel()) { + constraint->reference = true; + } + } + + if((id == Command::DISTANCE_DIA || id == Command::ANGLE || + id == Command::RATIO || id == Command::DIFFERENCE) && + SS.immediatelyEditDimension) { + SS.GW.EditConstraint(nc.h); + } + } + + SS.GW.ClearSelection(); +} + +#endif /* ! LIBRARY */ + +} // namespace SolveSpace diff --git a/src/constrainteq.cpp b/src/constrainteq.cpp new file mode 100644 index 0000000..644e480 --- /dev/null +++ b/src/constrainteq.cpp @@ -0,0 +1,1062 @@ +//----------------------------------------------------------------------------- +// Given a constraint, generate one or more equations in our symbolic algebra +// system to represent that constraint; also various geometric helper +// functions for that. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +const hConstraint ConstraintBase::NO_CONSTRAINT = { 0 }; + +bool ConstraintBase::HasLabel() const { + switch(type) { + case Type::PT_LINE_DISTANCE: + case Type::PT_PLANE_DISTANCE: + case Type::PT_FACE_DISTANCE: + case Type::PT_PT_DISTANCE: + case Type::PROJ_PT_DISTANCE: + case Type::DIAMETER: + case Type::LENGTH_RATIO: + case Type::ARC_ARC_LEN_RATIO: + case Type::ARC_LINE_LEN_RATIO: + case Type::LENGTH_DIFFERENCE: + case Type::ARC_ARC_DIFFERENCE: + case Type::ARC_LINE_DIFFERENCE: + case Type::ANGLE: + case Type::COMMENT: + return true; + + default: + return false; + } +} + +bool ConstraintBase::IsProjectible() const { + switch(type) { + case Type::POINTS_COINCIDENT: + case Type::PT_PT_DISTANCE: + case Type::PT_LINE_DISTANCE: + case Type::PT_ON_LINE: + case Type::EQUAL_LENGTH_LINES: + case Type::EQ_LEN_PT_LINE_D: + case Type::EQ_PT_LN_DISTANCES: + case Type::EQUAL_ANGLE: + case Type::LENGTH_RATIO: + case Type::ARC_ARC_LEN_RATIO: + case Type::ARC_LINE_LEN_RATIO: + case Type::LENGTH_DIFFERENCE: + case Type::ARC_ARC_DIFFERENCE: + case Type::ARC_LINE_DIFFERENCE: + case Type::SYMMETRIC: + case Type::SYMMETRIC_HORIZ: + case Type::SYMMETRIC_VERT: + case Type::SYMMETRIC_LINE: + case Type::AT_MIDPOINT: + case Type::HORIZONTAL: + case Type::VERTICAL: + case Type::ANGLE: + case Type::PARALLEL: + case Type::PERPENDICULAR: + case Type::WHERE_DRAGGED: + case Type::COMMENT: + return true; + + case Type::PT_PLANE_DISTANCE: + case Type::PT_FACE_DISTANCE: + case Type::PROJ_PT_DISTANCE: + case Type::PT_IN_PLANE: + case Type::PT_ON_FACE: + case Type::EQUAL_LINE_ARC_LEN: + case Type::DIAMETER: + case Type::PT_ON_CIRCLE: + case Type::SAME_ORIENTATION: + case Type::CUBIC_LINE_TANGENT: + case Type::CURVE_CURVE_TANGENT: + case Type::ARC_LINE_TANGENT: + case Type::EQUAL_RADIUS: + return false; + } + ssassert(false, "Impossible"); +} + +ExprVector ConstraintBase::VectorsParallel3d(ExprVector a, ExprVector b, hParam p) { + return a.Minus(b.ScaledBy(Expr::From(p))); +} + +Expr *ConstraintBase::PointLineDistance(hEntity wrkpl, hEntity hpt, hEntity hln) +{ + EntityBase *ln = SK.GetEntity(hln); + EntityBase *a = SK.GetEntity(ln->point[0]); + EntityBase *b = SK.GetEntity(ln->point[1]); + + EntityBase *p = SK.GetEntity(hpt); + + if(wrkpl == EntityBase::FREE_IN_3D) { + ExprVector ep = p->PointGetExprs(); + + ExprVector ea = a->PointGetExprs(); + ExprVector eb = b->PointGetExprs(); + ExprVector eab = ea.Minus(eb); + Expr *m = eab.Magnitude(); + + return ((eab.Cross(ea.Minus(ep))).Magnitude())->Div(m); + } else { + Expr *ua, *va, *ub, *vb; + a->PointGetExprsInWorkplane(wrkpl, &ua, &va); + b->PointGetExprsInWorkplane(wrkpl, &ub, &vb); + + Expr *du = ua->Minus(ub); + Expr *dv = va->Minus(vb); + + Expr *u, *v; + p->PointGetExprsInWorkplane(wrkpl, &u, &v); + + Expr *m = ((du->Square())->Plus(dv->Square()))->Sqrt(); + + Expr *proj = (dv->Times(ua->Minus(u)))->Minus( + (du->Times(va->Minus(v)))); + + return proj->Div(m); + } +} + +Expr *ConstraintBase::PointPlaneDistance(ExprVector p, hEntity hpl) { + ExprVector n; + Expr *d; + SK.GetEntity(hpl)->WorkplaneGetPlaneExprs(&n, &d); + return (p.Dot(n))->Minus(d); +} + +Expr *ConstraintBase::Distance(hEntity wrkpl, hEntity hpa, hEntity hpb) { + EntityBase *pa = SK.GetEntity(hpa); + EntityBase *pb = SK.GetEntity(hpb); + ssassert(pa->IsPoint() && pb->IsPoint(), + "Expected two points to measure projected distance between"); + + if(wrkpl == EntityBase::FREE_IN_3D) { + // This is true distance + ExprVector ea, eb, eab; + ea = pa->PointGetExprs(); + eb = pb->PointGetExprs(); + eab = ea.Minus(eb); + + return eab.Magnitude(); + } else { + // This is projected distance, in the given workplane. + Expr *au, *av, *bu, *bv; + + pa->PointGetExprsInWorkplane(wrkpl, &au, &av); + pb->PointGetExprsInWorkplane(wrkpl, &bu, &bv); + + Expr *du = au->Minus(bu); + Expr *dv = av->Minus(bv); + + return ((du->Square())->Plus(dv->Square()))->Sqrt(); + } +} + +//----------------------------------------------------------------------------- +// Return the cosine of the angle between two vectors. If a workplane is +// specified, then it's the cosine of their projections into that workplane. +//----------------------------------------------------------------------------- +Expr *ConstraintBase::DirectionCosine(hEntity wrkpl, + ExprVector ae, ExprVector be) +{ + if(wrkpl == EntityBase::FREE_IN_3D) { + Expr *mags = (ae.Magnitude())->Times(be.Magnitude()); + return (ae.Dot(be))->Div(mags); + } else { + EntityBase *w = SK.GetEntity(wrkpl); + ExprVector u = w->Normal()->NormalExprsU(); + ExprVector v = w->Normal()->NormalExprsV(); + Expr *ua = u.Dot(ae); + Expr *va = v.Dot(ae); + Expr *ub = u.Dot(be); + Expr *vb = v.Dot(be); + Expr *maga = (ua->Square()->Plus(va->Square()))->Sqrt(); + Expr *magb = (ub->Square()->Plus(vb->Square()))->Sqrt(); + Expr *dot = (ua->Times(ub))->Plus(va->Times(vb)); + return dot->Div(maga->Times(magb)); + } +} + +ExprVector ConstraintBase::PointInThreeSpace(hEntity workplane, + Expr *u, Expr *v) +{ + EntityBase *w = SK.GetEntity(workplane); + + ExprVector ub = w->Normal()->NormalExprsU(); + ExprVector vb = w->Normal()->NormalExprsV(); + ExprVector ob = w->WorkplaneGetOffsetExprs(); + + return (ub.ScaledBy(u)).Plus(vb.ScaledBy(v)).Plus(ob); +} + +void ConstraintBase::ModifyToSatisfy() { + if(type == Type::ANGLE) { + Vector a = SK.GetEntity(entityA)->VectorGetNum(); + Vector b = SK.GetEntity(entityB)->VectorGetNum(); + if(other) a = a.ScaledBy(-1); + if(workplane != EntityBase::FREE_IN_3D) { + a = a.ProjectVectorInto(workplane); + b = b.ProjectVectorInto(workplane); + } + double c = (a.Dot(b))/(a.Magnitude() * b.Magnitude()); + valA = acos(c)*180/PI; + } else if(type == Type::PT_ON_LINE) { + EntityBase *eln = SK.GetEntity(entityA); + EntityBase *ea = SK.GetEntity(eln->point[0]); + EntityBase *eb = SK.GetEntity(eln->point[1]); + EntityBase *ep = SK.GetEntity(ptA); + ExprVector exp = ep->PointGetExprsInWorkplane(workplane); + ExprVector exa = ea->PointGetExprsInWorkplane(workplane); + ExprVector exb = eb->PointGetExprsInWorkplane(workplane); + ExprVector exba = exb.Minus(exa); + SK.GetParam(valP)->val = exba.Dot(exp.Minus(exa))->Eval() / exba.Dot(exba)->Eval(); + } else { + // We'll fix these ones up by looking at their symbolic equation; + // that means no extra work. + IdList l = {}; + // Generate the equations even if this is a reference dimension + GenerateEquations(&l, /*forReference=*/true); + ssassert(l.n == 1, "Expected constraint to generate a single equation"); + + // These equations are written in the form f(...) - d = 0, where + // d is the value of the valA. + valA += (l[0].e)->Eval(); + + l.Clear(); + } +} + +void ConstraintBase::AddEq(IdList *l, Expr *expr, int index) const +{ + Equation eq; + eq.e = expr; + eq.h = h.equation(index); + l->Add(&eq); +} + +void ConstraintBase::AddEq(IdList *l, const ExprVector &v, + int baseIndex) const { + AddEq(l, v.x, baseIndex); + AddEq(l, v.y, baseIndex + 1); + if(workplane == EntityBase::FREE_IN_3D) { + AddEq(l, v.z, baseIndex + 2); + } +} + +void ConstraintBase::Generate(ParamList *l) { + switch(type) { + case Type::PARALLEL: + case Type::CUBIC_LINE_TANGENT: + // Add new parameter only when we operate in 3d space + if(workplane != EntityBase::FREE_IN_3D) break; + // fallthrough + case Type::SAME_ORIENTATION: + case Type::PT_ON_LINE: { + Param p = {}; + valP = h.param(0); + p.h = valP; + l->Add(&p); + break; + } + + default: + break; + } +} + +void ConstraintBase::GenerateEquations(IdList *l, + bool forReference) const { + if(reference && !forReference) return; + + Expr *exA = Expr::From(valA); + switch(type) { + case Type::PT_PT_DISTANCE: + AddEq(l, Distance(workplane, ptA, ptB)->Minus(exA), 0); + return; + + case Type::PROJ_PT_DISTANCE: { + ExprVector pA = SK.GetEntity(ptA)->PointGetExprs(), + pB = SK.GetEntity(ptB)->PointGetExprs(), + dp = pB.Minus(pA); + + ExprVector pp = SK.GetEntity(entityA)->VectorGetExprs(); + pp = pp.WithMagnitude(Expr::From(1.0)); + + AddEq(l, (dp.Dot(pp))->Minus(exA), 0); + return; + } + + case Type::PT_LINE_DISTANCE: + AddEq(l, + PointLineDistance(workplane, ptA, entityA)->Minus(exA), 0); + return; + + case Type::PT_PLANE_DISTANCE: { + ExprVector pt = SK.GetEntity(ptA)->PointGetExprs(); + AddEq(l, (PointPlaneDistance(pt, entityA))->Minus(exA), 0); + return; + } + + case Type::PT_FACE_DISTANCE: { + ExprVector pt = SK.GetEntity(ptA)->PointGetExprs(); + EntityBase *f = SK.GetEntity(entityA); + ExprVector p0 = f->FaceGetPointExprs(); + ExprVector n = f->FaceGetNormalExprs(); + AddEq(l, (pt.Minus(p0)).Dot(n)->Minus(exA), 0); + return; + } + + case Type::EQUAL_LENGTH_LINES: { + EntityBase *a = SK.GetEntity(entityA); + EntityBase *b = SK.GetEntity(entityB); + AddEq(l, Distance(workplane, a->point[0], a->point[1])->Minus( + Distance(workplane, b->point[0], b->point[1])), 0); + return; + } + + // These work on distance squared, since the pt-line distances are + // signed, and we want the absolute value. + case Type::EQ_LEN_PT_LINE_D: { + EntityBase *forLen = SK.GetEntity(entityA); + Expr *d1 = Distance(workplane, forLen->point[0], forLen->point[1]); + Expr *d2 = PointLineDistance(workplane, ptA, entityB); + AddEq(l, (d1->Square())->Minus(d2->Square()), 0); + return; + } + case Type::EQ_PT_LN_DISTANCES: { + Expr *d1 = PointLineDistance(workplane, ptA, entityA); + Expr *d2 = PointLineDistance(workplane, ptB, entityB); + AddEq(l, (d1->Square())->Minus(d2->Square()), 0); + return; + } + + case Type::LENGTH_RATIO: { + EntityBase *a = SK.GetEntity(entityA); + EntityBase *b = SK.GetEntity(entityB); + Expr *la = Distance(workplane, a->point[0], a->point[1]); + Expr *lb = Distance(workplane, b->point[0], b->point[1]); + AddEq(l, (la->Div(lb))->Minus(exA), 0); + return; + } + + case Type::ARC_ARC_LEN_RATIO: { + EntityBase *arc1 = SK.GetEntity(entityA), + *arc2 = SK.GetEntity(entityB); + + // And get the arc1 radius, and the cosine of its angle + EntityBase *ao1 = SK.GetEntity(arc1->point[0]), + *as1 = SK.GetEntity(arc1->point[1]), + *af1 = SK.GetEntity(arc1->point[2]); + + ExprVector aos1 = (as1->PointGetExprs()).Minus(ao1->PointGetExprs()), + aof1 = (af1->PointGetExprs()).Minus(ao1->PointGetExprs()); + Expr *r1 = aof1.Magnitude(); + + ExprVector n1 = arc1->Normal()->NormalExprsN(); + ExprVector u1 = aos1.WithMagnitude(Expr::From(1.0)); + ExprVector v1 = n1.Cross(u1); + // so in our new csys, we start at (1, 0, 0) + Expr *costheta1 = aof1.Dot(u1)->Div(r1); + Expr *sintheta1 = aof1.Dot(v1)->Div(r1); + + double thetas1, thetaf1, dtheta1; + arc1->ArcGetAngles(&thetas1, &thetaf1, &dtheta1); + Expr *theta1; + if(dtheta1 < 3*PI/4) { + theta1 = costheta1->ACos(); + } else if(dtheta1 < 5*PI/4) { + // As the angle crosses pi, cos theta1 is not invertible; + // so use the sine to stop blowing up + theta1 = Expr::From(PI)->Minus(sintheta1->ASin()); + } else { + theta1 = (Expr::From(2*PI))->Minus(costheta1->ACos()); + } + + // And get the arc2 radius, and the cosine of its angle + EntityBase *ao2 = SK.GetEntity(arc2->point[0]), + *as2 = SK.GetEntity(arc2->point[1]), + *af2 = SK.GetEntity(arc2->point[2]); + + ExprVector aos2 = (as2->PointGetExprs()).Minus(ao2->PointGetExprs()), + aof2 = (af2->PointGetExprs()).Minus(ao2->PointGetExprs()); + Expr *r2 = aof2.Magnitude(); + + ExprVector n2 = arc2->Normal()->NormalExprsN(); + ExprVector u2 = aos2.WithMagnitude(Expr::From(1.0)); + ExprVector v2 = n2.Cross(u2); + // so in our new csys, we start at (1, 0, 0) + Expr *costheta2 = aof2.Dot(u2)->Div(r2); + Expr *sintheta2 = aof2.Dot(v2)->Div(r2); + + double thetas2, thetaf2, dtheta2; + arc2->ArcGetAngles(&thetas2, &thetaf2, &dtheta2); + Expr *theta2; + if(dtheta2 < 3*PI/4) { + theta2 = costheta2->ACos(); + } else if(dtheta2 < 5*PI/4) { + // As the angle crosses pi, cos theta2 is not invertible; + // so use the sine to stop blowing up + theta2 = Expr::From(PI)->Minus(sintheta2->ASin()); + } else { + theta2 = (Expr::From(2*PI))->Minus(costheta2->ACos()); + } + // And write the equation; (r1*theta1) / ( r2*theta2) = some ratio + AddEq(l, (r1->Times(theta1))->Div(r2->Times(theta2))->Minus(exA), 0); + return; + } + + case Type::ARC_LINE_LEN_RATIO: { + EntityBase *line = SK.GetEntity(entityA), + *arc1 = SK.GetEntity(entityB); + + Expr *ll = Distance(workplane, line->point[0], line->point[1]); + + // And get the arc1 radius, and the cosine of its angle + EntityBase *ao1 = SK.GetEntity(arc1->point[0]), + *as1 = SK.GetEntity(arc1->point[1]), + *af1 = SK.GetEntity(arc1->point[2]); + + ExprVector aos1 = (as1->PointGetExprs()).Minus(ao1->PointGetExprs()), + aof1 = (af1->PointGetExprs()).Minus(ao1->PointGetExprs()); + Expr *r1 = aof1.Magnitude(); + ExprVector n1 = arc1->Normal()->NormalExprsN(); + ExprVector u1 = aos1.WithMagnitude(Expr::From(1.0)); + ExprVector v1 = n1.Cross(u1); + // so in our new csys, we start at (1, 0, 0) + Expr *costheta1 = aof1.Dot(u1)->Div(r1); + Expr *sintheta1 = aof1.Dot(v1)->Div(r1); + + double thetas1, thetaf1, dtheta1; + arc1->ArcGetAngles(&thetas1, &thetaf1, &dtheta1); + Expr *theta1; + if(dtheta1 < 3*PI/4) { + theta1 = costheta1->ACos(); + } else if(dtheta1 < 5*PI/4) { + // As the angle crosses pi, cos theta1 is not invertible; + // so use the sine to stop blowing up + theta1 = Expr::From(PI)->Minus(sintheta1->ASin()); + } else { + theta1 = (Expr::From(2*PI))->Minus(costheta1->ACos()); + } + // And write the equation; (r1*theta1) / ( length) = some ratio + AddEq(l, (r1->Times(theta1))->Div(ll)->Minus(exA), 0); + return; + } + + case Type::LENGTH_DIFFERENCE: { + EntityBase *a = SK.GetEntity(entityA); + EntityBase *b = SK.GetEntity(entityB); + Expr *la = Distance(workplane, a->point[0], a->point[1]); + Expr *lb = Distance(workplane, b->point[0], b->point[1]); + AddEq(l, (la->Minus(lb))->Minus(exA), 0); + return; + } + + case Type::ARC_ARC_DIFFERENCE: { + EntityBase *arc1 = SK.GetEntity(entityA), + *arc2 = SK.GetEntity(entityB); + + // And get the arc1 radius, and the cosine of its angle + EntityBase *ao1 = SK.GetEntity(arc1->point[0]), + *as1 = SK.GetEntity(arc1->point[1]), + *af1 = SK.GetEntity(arc1->point[2]); + + ExprVector aos1 = (as1->PointGetExprs()).Minus(ao1->PointGetExprs()), + aof1 = (af1->PointGetExprs()).Minus(ao1->PointGetExprs()); + Expr *r1 = aof1.Magnitude(); + + ExprVector n1 = arc1->Normal()->NormalExprsN(); + ExprVector u1 = aos1.WithMagnitude(Expr::From(1.0)); + ExprVector v1 = n1.Cross(u1); + // so in our new csys, we start at (1, 0, 0) + Expr *costheta1 = aof1.Dot(u1)->Div(r1); + Expr *sintheta1 = aof1.Dot(v1)->Div(r1); + + double thetas1, thetaf1, dtheta1; + arc1->ArcGetAngles(&thetas1, &thetaf1, &dtheta1); + Expr *theta1; + if(dtheta1 < 3*PI/4) { + theta1 = costheta1->ACos(); + } else if(dtheta1 < 5*PI/4) { + // As the angle crosses pi, cos theta1 is not invertible; + // so use the sine to stop blowing up + theta1 = Expr::From(PI)->Minus(sintheta1->ASin()); + } else { + theta1 = (Expr::From(2*PI))->Minus(costheta1->ACos()); + } + + // And get the arc2 radius, and the cosine of its angle + EntityBase *ao2 = SK.GetEntity(arc2->point[0]), + *as2 = SK.GetEntity(arc2->point[1]), + *af2 = SK.GetEntity(arc2->point[2]); + + ExprVector aos2 = (as2->PointGetExprs()).Minus(ao2->PointGetExprs()), + aof2 = (af2->PointGetExprs()).Minus(ao2->PointGetExprs()); + Expr *r2 = aof2.Magnitude(); + + ExprVector n2 = arc2->Normal()->NormalExprsN(); + ExprVector u2 = aos2.WithMagnitude(Expr::From(1.0)); + ExprVector v2 = n2.Cross(u2); + // so in our new csys, we start at (1, 0, 0) + Expr *costheta2 = aof2.Dot(u2)->Div(r2); + Expr *sintheta2 = aof2.Dot(v2)->Div(r2); + + double thetas2, thetaf2, dtheta2; + arc2->ArcGetAngles(&thetas2, &thetaf2, &dtheta2); + Expr *theta2; + if(dtheta2 < 3*PI/4) { + theta2 = costheta2->ACos(); + } else if(dtheta2 < 5*PI/4) { + // As the angle crosses pi, cos theta2 is not invertible; + // so use the sine to stop blowing up + theta2 = Expr::From(PI)->Minus(sintheta2->ASin()); + } else { + theta2 = (Expr::From(2*PI))->Minus(costheta2->ACos()); + } + // And write the equation; (r1*theta1) - ( r2*theta2) = some difference + AddEq(l, (r1->Times(theta1))->Minus(r2->Times(theta2))->Minus(exA), 0); + return; + } + + case Type::ARC_LINE_DIFFERENCE: { + EntityBase *line = SK.GetEntity(entityA), + *arc1 = SK.GetEntity(entityB); + + Expr *ll = Distance(workplane, line->point[0], line->point[1]); + + // And get the arc1 radius, and the cosine of its angle + EntityBase *ao1 = SK.GetEntity(arc1->point[0]), + *as1 = SK.GetEntity(arc1->point[1]), + *af1 = SK.GetEntity(arc1->point[2]); + + ExprVector aos1 = (as1->PointGetExprs()).Minus(ao1->PointGetExprs()), + aof1 = (af1->PointGetExprs()).Minus(ao1->PointGetExprs()); + Expr *r1 = aof1.Magnitude(); + ExprVector n1 = arc1->Normal()->NormalExprsN(); + ExprVector u1 = aos1.WithMagnitude(Expr::From(1.0)); + ExprVector v1 = n1.Cross(u1); + // so in our new csys, we start at (1, 0, 0) + Expr *costheta1 = aof1.Dot(u1)->Div(r1); + Expr *sintheta1 = aof1.Dot(v1)->Div(r1); + + double thetas1, thetaf1, dtheta1; + arc1->ArcGetAngles(&thetas1, &thetaf1, &dtheta1); + Expr *theta1; + if(dtheta1 < 3*PI/4) { + theta1 = costheta1->ACos(); + } else if(dtheta1 < 5*PI/4) { + // As the angle crosses pi, cos theta1 is not invertible; + // so use the sine to stop blowing up + theta1 = Expr::From(PI)->Minus(sintheta1->ASin()); + } else { + theta1 = (Expr::From(2*PI))->Minus(costheta1->ACos()); + } + // And write the equation; (r1*theta1) - ( length) = some difference + AddEq(l, (r1->Times(theta1))->Minus(ll)->Minus(exA), 0); + return; + } + + case Type::DIAMETER: { + EntityBase *circle = SK.GetEntity(entityA); + Expr *r = circle->CircleGetRadiusExpr(); + AddEq(l, (r->Times(Expr::From(2)))->Minus(exA), 0); + return; + } + + case Type::EQUAL_RADIUS: { + EntityBase *c1 = SK.GetEntity(entityA); + EntityBase *c2 = SK.GetEntity(entityB); + AddEq(l, (c1->CircleGetRadiusExpr())->Minus( + c2->CircleGetRadiusExpr()), 0); + return; + } + + case Type::EQUAL_LINE_ARC_LEN: { + EntityBase *line = SK.GetEntity(entityA), + *arc = SK.GetEntity(entityB); + + // Get the line length + ExprVector l0 = SK.GetEntity(line->point[0])->PointGetExprs(), + l1 = SK.GetEntity(line->point[1])->PointGetExprs(); + Expr *ll = (l1.Minus(l0)).Magnitude(); + + // And get the arc radius, and the cosine of its angle + EntityBase *ao = SK.GetEntity(arc->point[0]), + *as = SK.GetEntity(arc->point[1]), + *af = SK.GetEntity(arc->point[2]); + + ExprVector aos = (as->PointGetExprs()).Minus(ao->PointGetExprs()), + aof = (af->PointGetExprs()).Minus(ao->PointGetExprs()); + Expr *r = aof.Magnitude(); + + ExprVector n = arc->Normal()->NormalExprsN(); + ExprVector u = aos.WithMagnitude(Expr::From(1.0)); + ExprVector v = n.Cross(u); + // so in our new csys, we start at (1, 0, 0) + Expr *costheta = aof.Dot(u)->Div(r); + Expr *sintheta = aof.Dot(v)->Div(r); + + double thetas, thetaf, dtheta; + arc->ArcGetAngles(&thetas, &thetaf, &dtheta); + Expr *theta; + if(dtheta < 3*PI/4) { + theta = costheta->ACos(); + } else if(dtheta < 5*PI/4) { + // As the angle crosses pi, cos theta is not invertible; + // so use the sine to stop blowing up + theta = Expr::From(PI)->Minus(sintheta->ASin()); + } else { + theta = (Expr::From(2*PI))->Minus(costheta->ACos()); + } + + // And write the equation; r*theta = L + AddEq(l, (r->Times(theta))->Minus(ll), 0); + return; + } + + case Type::POINTS_COINCIDENT: { + EntityBase *a = SK.GetEntity(ptA); + EntityBase *b = SK.GetEntity(ptB); + if(workplane == EntityBase::FREE_IN_3D) { + ExprVector pa = a->PointGetExprs(); + ExprVector pb = b->PointGetExprs(); + AddEq(l, pa.x->Minus(pb.x), 0); + AddEq(l, pa.y->Minus(pb.y), 1); + AddEq(l, pa.z->Minus(pb.z), 2); + } else { + Expr *au, *av; + Expr *bu, *bv; + a->PointGetExprsInWorkplane(workplane, &au, &av); + b->PointGetExprsInWorkplane(workplane, &bu, &bv); + AddEq(l, au->Minus(bu), 0); + AddEq(l, av->Minus(bv), 1); + } + return; + } + + case Type::PT_IN_PLANE: + // This one works the same, whether projected or not. + AddEq(l, PointPlaneDistance( + SK.GetEntity(ptA)->PointGetExprs(), entityA), 0); + return; + + case Type::PT_ON_FACE: { + // a plane, n dot (p - p0) = 0 + ExprVector p = SK.GetEntity(ptA)->PointGetExprs(); + EntityBase *f = SK.GetEntity(entityA); + ExprVector p0 = f->FaceGetPointExprs(); + ExprVector n = f->FaceGetNormalExprs(); + AddEq(l, (p.Minus(p0)).Dot(n), 0); + return; + } + + case Type::PT_ON_LINE: { + EntityBase *ln = SK.GetEntity(entityA); + EntityBase *a = SK.GetEntity(ln->point[0]); + EntityBase *b = SK.GetEntity(ln->point[1]); + EntityBase *p = SK.GetEntity(ptA); + + ExprVector ep = p->PointGetExprsInWorkplane(workplane); + ExprVector ea = a->PointGetExprsInWorkplane(workplane); + ExprVector eb = b->PointGetExprsInWorkplane(workplane); + + ExprVector ptOnLine = ea.Plus(eb.Minus(ea).ScaledBy(Expr::From(valP))); + ExprVector eq = ptOnLine.Minus(ep); + + AddEq(l, eq); + return; + } + + case Type::PT_ON_CIRCLE: { + // This actually constrains the point to lie on the cylinder. + EntityBase *circle = SK.GetEntity(entityA); + ExprVector center = SK.GetEntity(circle->point[0])->PointGetExprs(); + ExprVector pt = SK.GetEntity(ptA)->PointGetExprs(); + EntityBase *normal = SK.GetEntity(circle->normal); + ExprVector u = normal->NormalExprsU(), + v = normal->NormalExprsV(); + + Expr *du = (center.Minus(pt)).Dot(u), + *dv = (center.Minus(pt)).Dot(v); + + Expr *r = circle->CircleGetRadiusExpr(); + + AddEq(l, du->Square()->Plus(dv->Square())->Sqrt()->Minus(r), 0); + return; + } + + case Type::AT_MIDPOINT: + if(workplane == EntityBase::FREE_IN_3D) { + EntityBase *ln = SK.GetEntity(entityA); + ExprVector a = SK.GetEntity(ln->point[0])->PointGetExprs(); + ExprVector b = SK.GetEntity(ln->point[1])->PointGetExprs(); + ExprVector m = (a.Plus(b)).ScaledBy(Expr::From(0.5)); + + if(ptA.v) { + ExprVector p = SK.GetEntity(ptA)->PointGetExprs(); + AddEq(l, (m.x)->Minus(p.x), 0); + AddEq(l, (m.y)->Minus(p.y), 1); + AddEq(l, (m.z)->Minus(p.z), 2); + } else { + AddEq(l, PointPlaneDistance(m, entityB), 0); + } + } else { + EntityBase *ln = SK.GetEntity(entityA); + EntityBase *a = SK.GetEntity(ln->point[0]); + EntityBase *b = SK.GetEntity(ln->point[1]); + + Expr *au, *av, *bu, *bv; + a->PointGetExprsInWorkplane(workplane, &au, &av); + b->PointGetExprsInWorkplane(workplane, &bu, &bv); + Expr *mu = Expr::From(0.5)->Times(au->Plus(bu)); + Expr *mv = Expr::From(0.5)->Times(av->Plus(bv)); + + if(ptA.v) { + EntityBase *p = SK.GetEntity(ptA); + Expr *pu, *pv; + p->PointGetExprsInWorkplane(workplane, &pu, &pv); + AddEq(l, pu->Minus(mu), 0); + AddEq(l, pv->Minus(mv), 1); + } else { + ExprVector m = PointInThreeSpace(workplane, mu, mv); + AddEq(l, PointPlaneDistance(m, entityB), 0); + } + } + return; + + case Type::SYMMETRIC: + if(workplane == EntityBase::FREE_IN_3D) { + EntityBase *plane = SK.GetEntity(entityA); + EntityBase *ea = SK.GetEntity(ptA); + EntityBase *eb = SK.GetEntity(ptB); + ExprVector a = ea->PointGetExprs(); + ExprVector b = eb->PointGetExprs(); + + // The midpoint of the line connecting the symmetric points + // lies on the plane of the symmetry. + ExprVector m = (a.Plus(b)).ScaledBy(Expr::From(0.5)); + AddEq(l, PointPlaneDistance(m, plane->h), 0); + + // And projected into the plane of symmetry, the points are + // coincident. + Expr *au, *av, *bu, *bv; + ea->PointGetExprsInWorkplane(plane->h, &au, &av); + eb->PointGetExprsInWorkplane(plane->h, &bu, &bv); + AddEq(l, au->Minus(bu), 1); + AddEq(l, av->Minus(bv), 2); + } else { + EntityBase *plane = SK.GetEntity(entityA); + EntityBase *a = SK.GetEntity(ptA); + EntityBase *b = SK.GetEntity(ptB); + + Expr *au, *av, *bu, *bv; + a->PointGetExprsInWorkplane(workplane, &au, &av); + b->PointGetExprsInWorkplane(workplane, &bu, &bv); + Expr *mu = Expr::From(0.5)->Times(au->Plus(bu)); + Expr *mv = Expr::From(0.5)->Times(av->Plus(bv)); + + ExprVector m = PointInThreeSpace(workplane, mu, mv); + AddEq(l, PointPlaneDistance(m, plane->h), 0); + + // Construct a vector within the workplane that is normal + // to the symmetry pane's normal (i.e., that lies in the + // plane of symmetry). The line connecting the points is + // perpendicular to that constructed vector. + EntityBase *w = SK.GetEntity(workplane); + ExprVector u = w->Normal()->NormalExprsU(); + ExprVector v = w->Normal()->NormalExprsV(); + + ExprVector pa = a->PointGetExprs(); + ExprVector pb = b->PointGetExprs(); + ExprVector n; + Expr *d; + plane->WorkplaneGetPlaneExprs(&n, &d); + AddEq(l, (n.Cross(u.Cross(v))).Dot(pa.Minus(pb)), 1); + } + return; + + case Type::SYMMETRIC_HORIZ: + case Type::SYMMETRIC_VERT: { + ssassert(workplane != Entity::FREE_IN_3D, + "Unexpected horizontal/vertical symmetric constraint in 3d"); + + EntityBase *a = SK.GetEntity(ptA); + EntityBase *b = SK.GetEntity(ptB); + + Expr *au, *av, *bu, *bv; + a->PointGetExprsInWorkplane(workplane, &au, &av); + b->PointGetExprsInWorkplane(workplane, &bu, &bv); + + if(type == Type::SYMMETRIC_HORIZ) { + AddEq(l, av->Minus(bv), 0); + AddEq(l, au->Plus(bu), 1); + } else { + AddEq(l, au->Minus(bu), 0); + AddEq(l, av->Plus(bv), 1); + } + return; + } + + case Type::SYMMETRIC_LINE: { + EntityBase *pa = SK.GetEntity(ptA); + EntityBase *pb = SK.GetEntity(ptB); + + Expr *pau, *pav, *pbu, *pbv; + pa->PointGetExprsInWorkplane(workplane, &pau, &pav); + pb->PointGetExprsInWorkplane(workplane, &pbu, &pbv); + + EntityBase *ln = SK.GetEntity(entityA); + EntityBase *la = SK.GetEntity(ln->point[0]); + EntityBase *lb = SK.GetEntity(ln->point[1]); + Expr *lau, *lav, *lbu, *lbv; + la->PointGetExprsInWorkplane(workplane, &lau, &lav); + lb->PointGetExprsInWorkplane(workplane, &lbu, &lbv); + + Expr *dpu = pbu->Minus(pau), *dpv = pbv->Minus(pav); + Expr *dlu = lbu->Minus(lau), *dlv = lbv->Minus(lav); + + // The line through the points is perpendicular to the line + // of symmetry. + AddEq(l, (dlu->Times(dpu))->Plus(dlv->Times(dpv)), 0); + + // And the signed distances of the points to the line are + // equal in magnitude and opposite in sign, so sum to zero + Expr *dista = (dlv->Times(lau->Minus(pau)))->Minus( + (dlu->Times(lav->Minus(pav)))); + Expr *distb = (dlv->Times(lau->Minus(pbu)))->Minus( + (dlu->Times(lav->Minus(pbv)))); + AddEq(l, dista->Plus(distb), 1); + + return; + } + + case Type::HORIZONTAL: + case Type::VERTICAL: { + ssassert(workplane != Entity::FREE_IN_3D, + "Unexpected horizontal/vertical constraint in 3d"); + + hEntity ha, hb; + if(entityA.v) { + EntityBase *e = SK.GetEntity(entityA); + ha = e->point[0]; + hb = e->point[1]; + } else { + ha = ptA; + hb = ptB; + } + EntityBase *a = SK.GetEntity(ha); + EntityBase *b = SK.GetEntity(hb); + + Expr *au, *av, *bu, *bv; + a->PointGetExprsInWorkplane(workplane, &au, &av); + b->PointGetExprsInWorkplane(workplane, &bu, &bv); + + AddEq(l, (type == Type::HORIZONTAL) ? av->Minus(bv) : au->Minus(bu), 0); + return; + } + + case Type::SAME_ORIENTATION: { + EntityBase *a = SK.GetEntity(entityA); + EntityBase *b = SK.GetEntity(entityB); + + ExprVector au = a->NormalExprsU(), + an = a->NormalExprsN(); + ExprVector bu = b->NormalExprsU(), + bv = b->NormalExprsV(), + bn = b->NormalExprsN(); + + ExprVector eq = VectorsParallel3d(an, bn, valP); + AddEq(l, eq.x, 0); + AddEq(l, eq.y, 1); + AddEq(l, eq.z, 2); + Expr *d1 = au.Dot(bv); + Expr *d2 = au.Dot(bu); + // Allow either orientation for the coordinate system, depending + // on how it was drawn. + if(fabs(d1->Eval()) < fabs(d2->Eval())) { + AddEq(l, d1, 3); + } else { + AddEq(l, d2, 3); + } + return; + } + + case Type::PERPENDICULAR: + case Type::ANGLE: { + EntityBase *a = SK.GetEntity(entityA); + EntityBase *b = SK.GetEntity(entityB); + ExprVector ae = a->VectorGetExprs(); + ExprVector be = b->VectorGetExprs(); + if(other) ae = ae.ScaledBy(Expr::From(-1)); + Expr *c = DirectionCosine(workplane, ae, be); + + if(type == Type::ANGLE) { + // The direction cosine is equal to the cosine of the + // specified angle + Expr *rads = exA->Times(Expr::From(PI/180)), + *rc = rads->Cos(); + double arc = fabs(rc->Eval()); + // avoid false detection of inconsistent systems by gaining + // up as the difference in dot products gets small at small + // angles; doubles still have plenty of precision, only + // problem is that rank test + Expr *mult = Expr::From(arc > 0.99 ? 0.01/(1.00001 - arc) : 1); + AddEq(l, (c->Minus(rc))->Times(mult), 0); + } else { + // The dot product (and therefore the direction cosine) + // is equal to zero, perpendicular. + AddEq(l, c, 0); + } + return; + } + + case Type::EQUAL_ANGLE: { + EntityBase *a = SK.GetEntity(entityA); + EntityBase *b = SK.GetEntity(entityB); + EntityBase *c = SK.GetEntity(entityC); + EntityBase *d = SK.GetEntity(entityD); + ExprVector ae = a->VectorGetExprs(); + ExprVector be = b->VectorGetExprs(); + ExprVector ce = c->VectorGetExprs(); + ExprVector de = d->VectorGetExprs(); + + if(other) ae = ae.ScaledBy(Expr::From(-1)); + + Expr *cab = DirectionCosine(workplane, ae, be); + Expr *ccd = DirectionCosine(workplane, ce, de); + + AddEq(l, cab->Minus(ccd), 0); + return; + } + + case Type::ARC_LINE_TANGENT: { + EntityBase *arc = SK.GetEntity(entityA); + EntityBase *line = SK.GetEntity(entityB); + + ExprVector ac = SK.GetEntity(arc->point[0])->PointGetExprs(); + ExprVector ap = + SK.GetEntity(arc->point[other ? 2 : 1])->PointGetExprs(); + + ExprVector ld = line->VectorGetExprs(); + + // The line is perpendicular to the radius + AddEq(l, ld.Dot(ac.Minus(ap)), 0); + return; + } + + case Type::CUBIC_LINE_TANGENT: { + EntityBase *cubic = SK.GetEntity(entityA); + EntityBase *line = SK.GetEntity(entityB); + + ExprVector a; + if(other) { + a = cubic->CubicGetFinishTangentExprs(); + } else { + a = cubic->CubicGetStartTangentExprs(); + } + + ExprVector b = line->VectorGetExprs(); + + if(workplane == EntityBase::FREE_IN_3D) { + ExprVector eq = VectorsParallel3d(a, b, valP); + AddEq(l, eq); + } else { + EntityBase *w = SK.GetEntity(workplane); + ExprVector wn = w->Normal()->NormalExprsN(); + AddEq(l, (a.Cross(b)).Dot(wn), 0); + } + return; + } + + case Type::CURVE_CURVE_TANGENT: { + bool parallel = true; + int i; + ExprVector dir[2]; + for(i = 0; i < 2; i++) { + EntityBase *e = SK.GetEntity((i == 0) ? entityA : entityB); + bool oth = (i == 0) ? other : other2; + + if(e->type == Entity::Type::ARC_OF_CIRCLE) { + ExprVector center, endpoint; + center = SK.GetEntity(e->point[0])->PointGetExprs(); + endpoint = + SK.GetEntity(e->point[oth ? 2 : 1])->PointGetExprs(); + dir[i] = endpoint.Minus(center); + // We're using the vector from the center of the arc to + // an endpoint; so that's normal to the tangent, not + // parallel. + parallel = !parallel; + } else if(e->type == Entity::Type::CUBIC) { // BRANCH_ALWAYS_TAKEN + if(oth) { + dir[i] = e->CubicGetFinishTangentExprs(); + } else { + dir[i] = e->CubicGetStartTangentExprs(); + } + } else { + ssassert(false, "Unexpected entity types for CURVE_CURVE_TANGENT"); + } + } + if(parallel) { + EntityBase *w = SK.GetEntity(workplane); + ExprVector wn = w->Normal()->NormalExprsN(); + AddEq(l, ((dir[0]).Cross(dir[1])).Dot(wn), 0); + } else { + AddEq(l, (dir[0]).Dot(dir[1]), 0); + } + return; + } + + case Type::PARALLEL: { + EntityBase *ea = SK.GetEntity(entityA), *eb = SK.GetEntity(entityB); + ExprVector a = ea->VectorGetExprsInWorkplane(workplane); + ExprVector b = eb->VectorGetExprsInWorkplane(workplane); + + if(workplane == EntityBase::FREE_IN_3D) { + ExprVector eq = VectorsParallel3d(a, b, valP); + AddEq(l, eq); + } else { + // We use expressions written in workplane csys, so we can assume the workplane + // normal is (0, 0, 1). We can write the equation as: + // Expr *eq = a.Cross(b).Dot(ExprVector::From(0.0, 0.0, 1.0)); + // but this will just result in elimination of x and y terms after dot product. + // We can only use the z expression: + // Expr *eq = a.Cross(b).z; + // but it's more efficient to write it in the terms of pseudo-scalar product: + Expr *eq = (a.x->Times(b.y))->Minus(a.y->Times(b.x)); + AddEq(l, eq, 0); + } + + return; + } + + case Type::WHERE_DRAGGED: { + EntityBase *ep = SK.GetEntity(ptA); + if(workplane == EntityBase::FREE_IN_3D) { + ExprVector ev = ep->PointGetExprs(); + Vector v = ep->PointGetNum(); + + AddEq(l, ev.x->Minus(Expr::From(v.x)), 0); + AddEq(l, ev.y->Minus(Expr::From(v.y)), 1); + AddEq(l, ev.z->Minus(Expr::From(v.z)), 2); + } else { + Expr *u, *v; + ep->PointGetExprsInWorkplane(workplane, &u, &v); + AddEq(l, u->Minus(Expr::From(u->Eval())), 0); + AddEq(l, v->Minus(Expr::From(v->Eval())), 1); + } + return; + } + + case Type::COMMENT: + return; + } + ssassert(false, "Unexpected constraint ID"); +} + +} // namespace SolveSpace diff --git a/src/defs.h b/src/defs.h new file mode 100644 index 0000000..94105f9 --- /dev/null +++ b/src/defs.h @@ -0,0 +1,15 @@ +#ifndef SOLVESPACE_DEFS_H +#define SOLVESPACE_DEFS_H + +namespace SolveSpace { + +static constexpr double ANGLE_COS_EPS = 1e-6; +static constexpr double LENGTH_EPS = 1e-6; +static constexpr double VERY_POSITIVE = 1e10; +static constexpr double VERY_NEGATIVE = -1e10; + +static constexpr double PI = 3.1415926535897931; + +} // namespace SolveSpace + +#endif // !SOLVESPACE_DEFS_H diff --git a/src/describescreen.cpp b/src/describescreen.cpp new file mode 100644 index 0000000..02a308a --- /dev/null +++ b/src/describescreen.cpp @@ -0,0 +1,589 @@ +//----------------------------------------------------------------------------- +// The screens when an entity is selected, that show some description of it-- +// endpoints of the lines, diameter of the circle, etc. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +void TextWindow::ScreenUnselectAll(int link, uint32_t v) { + GraphicsWindow::MenuEdit(Command::UNSELECT_ALL); +} + +void TextWindow::ScreenEditTtfText(int link, uint32_t v) { + hRequest hr = { v }; + Request *r = SK.GetRequest(hr); + + SS.TW.ShowEditControl(10, r->str); + SS.TW.edit.meaning = Edit::TTF_TEXT; + SS.TW.edit.request = hr; +} + +void TextWindow::ScreenToggleTtfKerning(int link, uint32_t v) { + hRequest hr = { v }; + Request *r = SK.GetRequest(hr); + + SS.UndoRemember(); + r->extraPoints = !r->extraPoints; + + SS.MarkGroupDirty(r->group); + SS.ScheduleShowTW(); +} + +void TextWindow::ScreenSetTtfFont(int link, uint32_t v) { + int i = (int)v; + if(i < 0) return; + if(i >= SS.fonts.l.n) return; + + SS.GW.GroupSelection(); + auto const &gs = SS.GW.gs; + if(gs.entities != 1 || gs.n != 1) return; + + Entity *e = SK.entity.FindByIdNoOops(gs.entity[0]); + if(!e || e->type != Entity::Type::TTF_TEXT || !e->h.isFromRequest()) return; + + Request *r = SK.request.FindByIdNoOops(e->h.request()); + if(!r) return; + + SS.UndoRemember(); + r->font = SS.fonts.l[i].FontFileBaseName(); + SS.MarkGroupDirty(r->group); + SS.ScheduleShowTW(); +} + +void TextWindow::ScreenConstraintToggleReference(int link, uint32_t v) { + hConstraint hc = { v }; + Constraint *c = SK.GetConstraint(hc); + + SS.UndoRemember(); + c->reference = !c->reference; + + SS.MarkGroupDirty(c->group); + SS.ScheduleShowTW(); +} + +void TextWindow::ScreenConstraintShowAsRadius(int link, uint32_t v) { + hConstraint hc = { v }; + Constraint *c = SK.GetConstraint(hc); + + SS.UndoRemember(); + c->other = !c->other; + + SS.ScheduleShowTW(); +} + +void TextWindow::DescribeSelection() { + Printf(false, ""); + +#define COSTR_NO_LINK(p) \ + SS.MmToString((p).x).c_str(), \ + SS.MmToString((p).y).c_str(), \ + SS.MmToString((p).z).c_str() +#define PT_AS_STR_NO_LINK "(%Fi%s%Fd, %Fi%s%Fd, %Fi%s%Fd)" +#define PT_AS_NUM "(%Fi%3%Fd, %Fi%3%Fd, %Fi%3%Fd)" +#define COSTR(e, p) \ + e->h, (&TextWindow::ScreenSelectEntity), (&TextWindow::ScreenHoverEntity), \ + COSTR_NO_LINK(p) +#define PT_AS_STR "%Ll%D%f%h" PT_AS_STR_NO_LINK "%E" +#define CO_LINK(e, p) e->h, (&TextWindow::ScreenSelectEntity), (&TextWindow::ScreenHoverEntity), CO(p) +#define PT_AS_NUM_LINK "%Ll%D%f%h" PT_AS_NUM "%E" + + auto const &gs = SS.GW.gs; + + auto ListFaces = [&]() { + char abc = 'A'; + for(auto &fc : gs.face) { + Vector n = SK.GetEntity(fc)->FaceGetNormalNum(); + Printf(true, " plane%c normal = " PT_AS_NUM, abc, CO(n)); + Vector p = SK.GetEntity(fc)->FaceGetPointNum(); + Printf(false, " plane%c thru = " PT_AS_STR, abc, COSTR(SK.GetEntity(fc), p)); + ++abc; + } + }; + + if(gs.n == 1 && (gs.points == 1 || gs.entities == 1)) { + Entity *e = SK.GetEntity(gs.points == 1 ? gs.point[0] : gs.entity[0]); + Vector p; + + switch(e->type) { + case Entity::Type::POINT_IN_3D: + case Entity::Type::POINT_IN_2D: + case Entity::Type::POINT_N_TRANS: + case Entity::Type::POINT_N_ROT_TRANS: + case Entity::Type::POINT_N_COPY: + case Entity::Type::POINT_N_ROT_AA: + case Entity::Type::POINT_N_ROT_AXIS_TRANS: + p = e->PointGetNum(); + Printf(false, "%FtPOINT%E at " PT_AS_STR, COSTR(e, p)); + break; + + case Entity::Type::NORMAL_IN_3D: + case Entity::Type::NORMAL_IN_2D: + case Entity::Type::NORMAL_N_COPY: + case Entity::Type::NORMAL_N_ROT: + case Entity::Type::NORMAL_N_ROT_AA: { + Quaternion q = e->NormalGetNum(); + p = q.RotationN(); + Printf(false, "%FtNORMAL / COORDINATE SYSTEM%E"); + Printf(true, " basis n = " PT_AS_NUM, CO(p)); + p = q.RotationU(); + Printf(false, " u = " PT_AS_NUM, CO(p)); + p = q.RotationV(); + Printf(false, " v = " PT_AS_NUM, CO(p)); + break; + } + case Entity::Type::WORKPLANE: { + p = SK.GetEntity(e->point[0])->PointGetNum(); + Printf(false, "%FtWORKPLANE%E"); + Printf(true, " origin = " PT_AS_STR, COSTR(SK.GetEntity(e->point[0]), p)); + Quaternion q = e->Normal()->NormalGetNum(); + p = q.RotationN(); + Printf(true, " normal = " PT_AS_NUM_LINK, CO_LINK(e->Normal(), p)); + break; + } + case Entity::Type::LINE_SEGMENT: { + Vector p0 = SK.GetEntity(e->point[0])->PointGetNum(); + p = p0; + Printf(false, "%FtLINE SEGMENT%E"); + Printf(true, " thru " PT_AS_STR, COSTR(SK.GetEntity(e->point[0]), p)); + Vector p1 = SK.GetEntity(e->point[1])->PointGetNum(); + p = p1; + Printf(false, " " PT_AS_STR, COSTR(SK.GetEntity(e->point[1]), p)); + Printf(true, " len = %Fi%s%E", + SS.MmToString((p1.Minus(p0).Magnitude())).c_str()); + break; + } + case Entity::Type::CUBIC_PERIODIC: + case Entity::Type::CUBIC: + int pts; + if(e->type == Entity::Type::CUBIC_PERIODIC) { + Printf(false, "%FtPERIODIC C2 CUBIC SPLINE%E"); + pts = (3 + e->extraPoints); + } else if(e->extraPoints > 0) { + Printf(false, "%FtINTERPOLATING C2 CUBIC SPLINE%E"); + pts = (4 + e->extraPoints); + } else { + Printf(false, "%FtCUBIC BEZIER CURVE%E"); + pts = 4; + } + for(int i = 0; i < pts; i++) { + p = SK.GetEntity(e->point[i])->PointGetNum(); + Printf((i==0), " p%d = " PT_AS_STR, i, COSTR(SK.GetEntity(e->point[i]), p)); + } + break; + + case Entity::Type::ARC_OF_CIRCLE: { + Printf(false, "%FtARC OF A CIRCLE%E"); + p = SK.GetEntity(e->point[0])->PointGetNum(); + Printf(true, " center = " PT_AS_STR, COSTR(SK.GetEntity(e->point[0]), p)); + p = SK.GetEntity(e->point[1])->PointGetNum(); + Printf(true, " endpoints = " PT_AS_STR, COSTR(SK.GetEntity(e->point[1]), p)); + p = SK.GetEntity(e->point[2])->PointGetNum(); + Printf(false, " " PT_AS_STR, COSTR(SK.GetEntity(e->point[2]), p)); + double r = e->CircleGetRadiusNum(); + Printf(true, " diameter = %Fi%s", SS.MmToString(r*2).c_str()); + Printf(false, " radius = %Fi%s", SS.MmToString(r).c_str()); + double thetas, thetaf, dtheta; + e->ArcGetAngles(&thetas, &thetaf, &dtheta); + Printf(false, " arc len = %Fi%s", SS.MmToString(dtheta*r).c_str()); + break; + } + case Entity::Type::CIRCLE: { + Printf(false, "%FtCIRCLE%E"); + p = SK.GetEntity(e->point[0])->PointGetNum(); + Printf(true, " center = " PT_AS_STR, COSTR(SK.GetEntity(e->point[0]), p)); + double r = e->CircleGetRadiusNum(); + Printf(true, " diameter = %Fi%s", SS.MmToString(r*2).c_str()); + Printf(false, " radius = %Fi%s", SS.MmToString(r).c_str()); + Printf(false, " circumference = %Fi%s", SS.MmToString(2*M_PI*r).c_str()); + break; + } + case Entity::Type::FACE_NORMAL_PT: + case Entity::Type::FACE_XPROD: + case Entity::Type::FACE_N_ROT_TRANS: + case Entity::Type::FACE_N_ROT_AA: + case Entity::Type::FACE_N_TRANS: + case Entity::Type::FACE_ROT_NORMAL_PT: + case Entity::Type::FACE_N_ROT_AXIS_TRANS: + Printf(false, "%FtPLANE FACE%E"); + p = e->FaceGetNormalNum(); + Printf(true, " normal = " PT_AS_NUM, CO(p)); + p = e->FaceGetPointNum(); + Printf(false, " thru = " PT_AS_STR, COSTR(e, p)); + break; + + case Entity::Type::TTF_TEXT: { + Printf(false, "%FtTRUETYPE FONT TEXT%E"); + Printf(true, " font = '%Fi%s%E'", e->font.c_str()); + if(e->h.isFromRequest()) { + Printf(true, " text = '%Fi%s%E' %Fl%Ll%f%D[change]%E", + e->str.c_str(), &ScreenEditTtfText, e->h.request().v); + Printf(true, " %Fd%f%D%Ll%s apply kerning", + &ScreenToggleTtfKerning, e->h.request().v, + e->extraPoints ? CHECK_TRUE : CHECK_FALSE); + Printf(true, " select new font"); + SS.fonts.LoadAll(); + // Not using range-for here because we use i inside the output. + for(int i = 0; i < SS.fonts.l.n; i++) { + TtfFont *tf = &(SS.fonts.l[i]); + if(e->font == tf->FontFileBaseName()) { + Printf(false, "%Bp %s", + (i & 1) ? 'd' : 'a', + tf->name.c_str()); + } else { + Printf(false, "%Bp %f%D%Fl%Ll%s%E%Bp", + (i & 1) ? 'd' : 'a', + &ScreenSetTtfFont, i, + tf->name.c_str(), + (i & 1) ? 'd' : 'a'); + } + } + } else { + Printf(false, " text = '%Fi%s%E'", e->str.c_str()); + } + break; + } + case Entity::Type::IMAGE: { + Printf(false, "%FtIMAGE%E"); + Platform::Path relativePath = e->file.RelativeTo(SS.saveFile.Parent()); + if(relativePath.IsEmpty()) { + Printf(true, " file = '%Fi%s%E'", e->file.raw.c_str()); + } else { + Printf(true, " file = '%Fi%s%E'", relativePath.raw.c_str()); + } + break; + } + + default: + Printf(true, "%Ft?? ENTITY%E"); + break; + } + + Printf(false, ""); + if(e->h.isFromRequest()) { + Request *r = SK.GetRequest(e->h.request()); + if(e->h == r->h.entity(0)) { + Printf(false, "%FtFROM REQUEST%E %s", + r->DescriptionString().c_str()); + } else { + Printf(false, "%FtFROM REQUEST%E %Fl%Ll%D%f%h%s%E", + r->h.v, (&TextWindow::ScreenSelectRequest), &(TextWindow::ScreenHoverRequest), + r->DescriptionString().c_str()); + } + } + Group *g = SK.GetGroup(e->group); + Printf(false, "%FtIN GROUP%E %Fl%Ll%D%f%s%E", + g->h.v, (&TextWindow::ScreenSelectGroup), + g->DescriptionString().c_str()); + if(e->workplane == Entity::FREE_IN_3D) { + Printf(false, "%FtNOT LOCKED IN WORKPLANE%E"); + } else { + Entity *w = SK.GetEntity(e->workplane); + if(w->h.isFromRequest()) { + Printf(false, "%FtIN WORKPLANE%E %Fl%Ll%D%f%h%s%E", + w->h.request().v, + (&TextWindow::ScreenSelectRequest), &(TextWindow::ScreenHoverRequest), + w->DescriptionString().c_str()); + } else { + Printf(false, "%FtIN WORKPLANE%E %Fl%Ll%D%f%h%s%E", + w->h.group().v, + (&TextWindow::ScreenSelectGroup), (&TextWindow::ScreenHoverGroupWorkplane), + w->DescriptionString().c_str()); + } + } + if(e->IsStylable()) { + if(e->style.v) { + Style *s = Style::Get(e->style); + Printf(false, "%FtIN STYLE%E %Fl%Ll%D%f%s%E", + s->h.v, (&TextWindow::ScreenShowStyleInfo), + s->DescriptionString().c_str()); + } else { + Printf(false, "%FtIN STYLE%E none"); + } + } + if(e->construction) { + Printf(false, "%FtCONSTRUCTION"); + } + + std::vector lhc = {}; + auto FindConstraints = [&](hEntity he) { + for(const Constraint &c : SK.constraint) { + if(!(c.ptA == he || c.ptB == he || + c.entityA == he || c.entityB == he || c.entityC == he || c.entityD == he)) + continue; + lhc.push_back(c.h); + } + }; + FindConstraints(e->h); + if(!e->IsPoint()) { + for(int i = 0; i < MAX_POINTS_IN_ENTITY; i++) { + if(e->point[i].v == 0) break; + FindConstraints(e->point[i]); + } + } + + std::sort(lhc.begin(), lhc.end()); + lhc.erase(std::unique(lhc.begin(), lhc.end()), lhc.end()); + + auto ListConstraints = [&](bool reference) { + bool first = true; + int a = 0; + for(hConstraint hc : lhc) { + Constraint *c = SK.GetConstraint(hc); + if(c->reference != reference) continue; + if(first) { + first = false; + if(reference) { + Printf(true, "%FtMEASURED BY:%E"); + } else { + Printf(true, "%FtCONSTRAINED BY:%E"); + } + } + Printf(false, "%Bp %Fl%Ll%D%f%h%s%E", + (a & 1) ? 'd' : 'a', + c->h.v, (&TextWindow::ScreenSelectConstraint), + (&TextWindow::ScreenHoverConstraint), + c->DescriptionString().c_str()); + a++; + } + }; + ListConstraints(/*reference=*/false); + ListConstraints(/*reference=*/true); + } else if(gs.n == 2 && gs.points == 2) { + Printf(false, "%FtTWO POINTS"); + Vector p0 = SK.GetEntity(gs.point[0])->PointGetNum(); + Printf(true, " at " PT_AS_STR, COSTR(SK.GetEntity(gs.point[0]), p0)); + Vector p1 = SK.GetEntity(gs.point[1])->PointGetNum(); + Printf(false, " " PT_AS_STR, COSTR(SK.GetEntity(gs.point[1]), p1)); + Vector dv = p1.Minus(p0); + Printf(true, " d = %Fi%s", SS.MmToString(dv.Magnitude()).c_str()); + Printf(false, " d(x, y, z) = " PT_AS_STR_NO_LINK, COSTR_NO_LINK(dv)); + } else if(gs.n == 2 && gs.points == 1 && gs.circlesOrArcs == 1) { + Entity *ec = SK.GetEntity(gs.entity[0]); + if(ec->type == Entity::Type::CIRCLE) { + Printf(false, "%FtPOINT AND A CIRCLE"); + } else if(ec->type == Entity::Type::ARC_OF_CIRCLE) { + Printf(false, "%FtPOINT AND AN ARC"); + } else ssassert(false, "Unexpected entity type"); + Vector p = SK.GetEntity(gs.point[0])->PointGetNum(); + Printf(true, " pt at " PT_AS_STR, COSTR(SK.GetEntity(gs.point[0]), p)); + Vector c = SK.GetEntity(ec->point[0])->PointGetNum(); + Printf(true, " center = " PT_AS_STR, COSTR(SK.GetEntity(ec->point[0]), c)); + double r = ec->CircleGetRadiusNum(); + Printf(false, " diameter = %Fi%s", SS.MmToString(r*2).c_str()); + Printf(false, " radius = %Fi%s", SS.MmToString(r).c_str()); + double d = (p.Minus(c)).Magnitude() - r; + Printf(true, " distance = %Fi%s", SS.MmToString(d).c_str()); + } else if(gs.n == 2 && gs.faces == 1 && gs.points == 1) { + Printf(false, "%FtA POINT AND A PLANE FACE"); + Vector pt = SK.GetEntity(gs.point[0])->PointGetNum(); + Printf(true, " point = " PT_AS_STR, COSTR(SK.GetEntity(gs.point[0]), pt)); + Vector n = SK.GetEntity(gs.face[0])->FaceGetNormalNum(); + Printf(true, " plane normal = " PT_AS_NUM, CO(n)); + Vector pl = SK.GetEntity(gs.face[0])->FaceGetPointNum(); + Printf(false, " plane thru = " PT_AS_STR, COSTR(SK.GetEntity(gs.face[0]), pl)); + double dd = n.Dot(pl) - n.Dot(pt); + Printf(true, " distance = %Fi%s", SS.MmToString(dd).c_str()); + } else if(gs.n == 3 && gs.points == 2 && gs.vectors == 1) { + Printf(false, "%FtTWO POINTS AND A VECTOR"); + Vector p0 = SK.GetEntity(gs.point[0])->PointGetNum(); + Printf(true, " pointA = " PT_AS_STR, COSTR(SK.GetEntity(gs.point[0]), p0)); + Vector p1 = SK.GetEntity(gs.point[1])->PointGetNum(); + Printf(false, " pointB = " PT_AS_STR, COSTR(SK.GetEntity(gs.point[1]), p1)); + Vector v = SK.GetEntity(gs.vector[0])->VectorGetNum(); + v = v.WithMagnitude(1); + Printf(true, " vector = " PT_AS_NUM_LINK, CO_LINK(SK.GetEntity(gs.vector[0]), v)); + double d = (p1.Minus(p0)).Dot(v); + Printf(true, " proj_d = %Fi%s", SS.MmToString(d).c_str()); + } else if(gs.n == 2 && gs.lineSegments == 1 && gs.points == 1) { + Entity *ln = SK.GetEntity(gs.entity[0]); + Vector lp0 = SK.GetEntity(ln->point[0])->PointGetNum(), + lp1 = SK.GetEntity(ln->point[1])->PointGetNum(); + Printf(false, "%FtLINE SEGMENT AND POINT%E"); + Printf(true, " ln thru " PT_AS_STR, COSTR(SK.GetEntity(ln->point[0]), lp0)); + Printf(false, " " PT_AS_STR, COSTR(SK.GetEntity(ln->point[1]), lp1)); + Entity *p = SK.GetEntity(gs.point[0]); + Vector pp = p->PointGetNum(); + Printf(true, " point " PT_AS_STR, COSTR(p, pp)); + Printf(true, " pt-ln distance = %Fi%s%E", + SS.MmToString(pp.DistanceToLine(lp0, lp1.Minus(lp0))).c_str()); + hEntity wrkpl = SS.GW.ActiveWorkplane(); + if(wrkpl != Entity::FREE_IN_3D && !(p->workplane == wrkpl && ln->workplane == wrkpl)) { + Vector ppw = pp.ProjectInto(wrkpl); + Vector lp0w = lp0.ProjectInto(wrkpl); + Vector lp1w = lp1.ProjectInto(wrkpl); + Printf(false, " or distance = %Fi%s%E (in workplane)", + SS.MmToString(ppw.DistanceToLine(lp0w, lp1w.Minus(lp0w))).c_str()); + } + } else if(gs.n == 2 && gs.vectors == 2) { + Printf(false, "%FtTWO VECTORS"); + + Vector v0 = SK.GetEntity(gs.entity[0])->VectorGetNum(), + v1 = SK.GetEntity(gs.entity[1])->VectorGetNum(); + v0 = v0.WithMagnitude(1); + v1 = v1.WithMagnitude(1); + + Printf(true, " vectorA = " PT_AS_NUM_LINK, CO_LINK(SK.GetEntity(gs.entity[0]), v0)); + Printf(false, " vectorB = " PT_AS_NUM_LINK, CO_LINK(SK.GetEntity(gs.entity[1]), v1)); + + double theta = acos(v0.Dot(v1)); + Printf(true, " angle = %Fi%2%E degrees", theta*180/PI); + while(theta < PI/2) theta += PI; + while(theta > PI/2) theta -= PI; + Printf(false, " or angle = %Fi%2%E (mod 180)", theta*180/PI); + } else if(gs.n == 2 && gs.faces == 2) { + Printf(false, "%FtTWO PLANE FACES"); + + ListFaces(); + + Vector n0 = SK.GetEntity(gs.face[0])->FaceGetNormalNum(); + Vector n1 = SK.GetEntity(gs.face[1])->FaceGetNormalNum(); + + double theta = acos(n0.Dot(n1)); + Printf(true, " angle = %Fi%2%E degrees", theta*180/PI); + while(theta < PI/2) theta += PI; + while(theta > PI/2) theta -= PI; + Printf(false, " or angle = %Fi%2%E (mod 180)", theta*180/PI); + + if(fabs(theta) < 0.01) { + Vector p0 = SK.GetEntity(gs.face[0])->FaceGetPointNum(); + Vector p1 = SK.GetEntity(gs.face[1])->FaceGetPointNum(); + + double d = (p1.Minus(p0)).Dot(n0); + Printf(true, " distance = %Fi%s", SS.MmToString(d).c_str()); + } + } else if(gs.n == 3 && gs.faces == 3) { + Printf(false, "%FtTHREE PLANE FACES"); + + ListFaces(); + + // We should probably compute and show the intersection point if there is one. + + } else if(gs.n == 0 && gs.constraints == 1) { + Constraint *c = SK.GetConstraint(gs.constraint[0]); + const std::string &desc = c->DescriptionString().c_str(); + + if(c->type == Constraint::Type::COMMENT) { + Printf(false, "%FtCOMMENT%E %s", desc.c_str()); + if(c->ptA != Entity::NO_ENTITY) { + Vector p = SK.GetEntity(c->ptA)->PointGetNum(); + Printf(true, " attached to point at: " PT_AS_STR, COSTR(SK.GetEntity(c->ptA), p)); + Vector dv = c->disp.offset; + Printf(false, " distance = %Fi%s", SS.MmToString(dv.Magnitude()).c_str()); + Printf(false, " d(x, y, z) = " PT_AS_STR_NO_LINK, COSTR_NO_LINK(dv)); + } + } else if(c->HasLabel()) { + if(c->reference) { + Printf(false, "%FtREFERENCE%E %s", desc.c_str()); + } else { + Printf(false, "%FtDIMENSION%E %s", desc.c_str()); + } + Printf(true, " %Fd%f%D%Ll%s reference", + &ScreenConstraintToggleReference, gs.constraint[0].v, + c->reference ? CHECK_TRUE : CHECK_FALSE); + if(c->type == Constraint::Type::DIAMETER) { + Printf(false, " %Fd%f%D%Ll%s use radius", + &ScreenConstraintShowAsRadius, gs.constraint[0].v, + c->other ? CHECK_TRUE : CHECK_FALSE); + } + } else { + Printf(false, "%FtCONSTRAINT%E %s", desc.c_str()); + } + + if(c->IsProjectible()) { + if(c->workplane == Entity::FREE_IN_3D) { + Printf(true, "%FtNOT PROJECTED TO WORKPLANE%E"); + } else { + Entity *w = SK.GetEntity(c->workplane); + if(w->h.isFromRequest()) { + Printf(true, "%FtIN WORKPLANE%E %Fl%Ll%D%f%h%s%E", + w->h.request().v, + (&TextWindow::ScreenSelectRequest), &(TextWindow::ScreenHoverRequest), + w->DescriptionString().c_str()); + } else { + Printf(true, "%FtIN WORKPLANE%E %Fl%Ll%D%f%h%s%E", + w->h.group().v, + (&TextWindow::ScreenSelectGroup), (&TextWindow::ScreenHoverGroupWorkplane), + w->DescriptionString().c_str()); + } + } + } + + std::vector lhe = {}; + lhe.push_back(c->ptA); + lhe.push_back(c->ptB); + lhe.push_back(c->entityA); + lhe.push_back(c->entityB); + lhe.push_back(c->entityC); + lhe.push_back(c->entityD); + + auto it = std::remove_if(lhe.begin(), lhe.end(), [](hEntity he) { + return he == Entity::NO_ENTITY || !he.isFromRequest(); + }); + lhe.erase(it, lhe.end()); + + if(!lhe.empty()) { + if(c->reference) { + Printf(true, "%FtMEASURES:%E"); + } else { + Printf(true, "%FtCONSTRAINS:%E"); + } + + int a = 0; + for(hEntity he : lhe) { + Entity *e = SK.GetEntity(he); + Printf(false, "%Bp %Fl%Ll%D%f%h%s%E", + (a & 1) ? 'd' : 'a', + e->h.v, (&TextWindow::ScreenSelectEntity), + &(TextWindow::ScreenHoverEntity), + e->DescriptionString().c_str()); + a++; + } + } + } else { + int n = SS.GW.selection.n; + Printf(false, "%FtSELECTED:%E %d item%s", n, n == 1 ? "" : "s"); + } + + if(shown.screen == Screen::STYLE_INFO && + shown.style.v >= Style::FIRST_CUSTOM && gs.stylables > 0) + { + // If we are showing a screen for a particular style, then offer the + // option to assign our selected entities to that style. + Style *s = Style::Get(shown.style); + Printf(true, "%Fl%D%f%Ll(assign to style %s)%E", + shown.style.v, + &ScreenAssignSelectionToStyle, + s->DescriptionString().c_str()); + } + // If any of the selected entities have an assigned style, then offer + // the option to remove that style. + bool styleAssigned = false; + for(int i = 0; i < gs.entities; i++) { + Entity *e = SK.GetEntity(gs.entity[i]); + if(e->style.v != 0) { + styleAssigned = true; + } + } + for(int i = 0; i < gs.constraints; i++) { + Constraint *c = SK.GetConstraint(gs.constraint[i]); + if(c->type == Constraint::Type::COMMENT && c->disp.style.v != 0) { + styleAssigned = true; + } + } + if(styleAssigned) { + Printf(true, "%Fl%D%f%Ll(remove assigned style)%E", + 0, + &ScreenAssignSelectionToStyle); + } + + Printf(true, "%Fl%f%Ll(unselect all)%E", &TextWindow::ScreenUnselectAll); +} + +void TextWindow::GoToScreen(Screen screen) { + shown.screen = screen; +} + +} // namespace SolveSpace diff --git a/src/draw.cpp b/src/draw.cpp new file mode 100644 index 0000000..65603dd --- /dev/null +++ b/src/draw.cpp @@ -0,0 +1,961 @@ +//----------------------------------------------------------------------------- +// The root function to paint our graphics window, after setting up all the +// views and such appropriately. Also contains all the stuff to manage the +// selection. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +bool GraphicsWindow::Selection::Equals(Selection *b) { + if(entity != b->entity) return false; + if(constraint != b->constraint) return false; + return true; +} + +bool GraphicsWindow::Selection::IsEmpty() { + if(entity.v) return false; + if(constraint.v) return false; + return true; +} + +bool GraphicsWindow::Selection::HasEndpoints() { + if(!entity.v) return false; + Entity *e = SK.GetEntity(entity); + return e->HasEndpoints(); +} + +void GraphicsWindow::Selection::Clear() { + entity.v = constraint.v = 0; + emphasized = false; +} + +void GraphicsWindow::Selection::Draw(bool isHovered, Canvas *canvas) { + const Camera &camera = canvas->GetCamera(); + + std::vector refs; + if(entity.v) { + Entity *e = SK.GetEntity(entity); + e->Draw(isHovered ? Entity::DrawAs::HOVERED : + Entity::DrawAs::SELECTED, + canvas); + if(emphasized) { + e->GetReferencePoints(&refs); + } + } + if(constraint.v) { + Constraint *c = SK.GetConstraint(constraint); + c->Draw(isHovered ? Constraint::DrawAs::HOVERED : + Constraint::DrawAs::SELECTED, + canvas); + if(emphasized) { + c->GetReferencePoints(camera, &refs); + } + } + if(emphasized && (constraint.v || entity.v)) { + // We want to emphasize this constraint or entity, by drawing a thick + // line from the top left corner of the screen to the reference point(s) + // of that entity or constraint. + Canvas::Stroke strokeEmphasis = {}; + strokeEmphasis.layer = Canvas::Layer::FRONT; + strokeEmphasis.color = Style::Color(Style::HOVERED).WithAlpha(50); + strokeEmphasis.width = 40; + strokeEmphasis.unit = Canvas::Unit::PX; + Canvas::hStroke hcsEmphasis = canvas->GetStroke(strokeEmphasis); + + Point2d topLeftScreen; + topLeftScreen.x = -(double)camera.width / 2; + topLeftScreen.y = (double)camera.height / 2; + Vector topLeft = camera.UnProjectPoint(topLeftScreen); + + auto it = std::unique(refs.begin(), refs.end(), + [](const Vector &a, const Vector &b) { return a.Equals(b); }); + refs.erase(it, refs.end()); + for(const Vector &p : refs) { + canvas->DrawLine(topLeft, p, hcsEmphasis); + } + } +} + +void GraphicsWindow::ClearSelection() { + selection.Clear(); + SS.ScheduleShowTW(); + Invalidate(); +} + +void GraphicsWindow::ClearNonexistentSelectionItems() { + bool change = false; + Selection *s; + selection.ClearTags(); + for(s = selection.First(); s; s = selection.NextAfter(s)) { + if(s->constraint.v && !(SK.constraint.FindByIdNoOops(s->constraint))) { + s->tag = 1; + change = true; + } + if(s->entity.v && !(SK.entity.FindByIdNoOops(s->entity))) { + s->tag = 1; + change = true; + } + } + selection.RemoveTagged(); + if(change) Invalidate(); +} + +//----------------------------------------------------------------------------- +// Is this entity/constraint selected? +//----------------------------------------------------------------------------- +bool GraphicsWindow::IsSelected(hEntity he) { + Selection s = {}; + s.entity = he; + return IsSelected(&s); +} +bool GraphicsWindow::IsSelected(Selection *st) { + Selection *s; + for(s = selection.First(); s; s = selection.NextAfter(s)) { + if(s->Equals(st)) { + return true; + } + } + return false; +} + +//----------------------------------------------------------------------------- +// Unselect an item, if it is selected. We can either unselect just that item, +// or also unselect any coincident points. The latter is useful if the user +// somehow selects two coincident points (like with select all), because it +// would otherwise be impossible to de-select the lower of the two. +//----------------------------------------------------------------------------- +void GraphicsWindow::MakeUnselected(hEntity he, bool coincidentPointTrick) { + Selection stog = {}; + stog.entity = he; + MakeUnselected(&stog, coincidentPointTrick); +} +void GraphicsWindow::MakeUnselected(Selection *stog, bool coincidentPointTrick){ + if(stog->IsEmpty()) return; + + Selection *s; + + // If an item was selected, then we just un-select it. + selection.ClearTags(); + for(s = selection.First(); s; s = selection.NextAfter(s)) { + if(s->Equals(stog)) { + s->tag = 1; + } + } + // If two points are coincident, then it's impossible to hover one of + // them. But make sure to deselect both, to avoid mysterious seeming + // inability to deselect if the bottom one did somehow get selected. + if(stog->entity.v && coincidentPointTrick) { + Entity *e = SK.GetEntity(stog->entity); + if(e->IsPoint()) { + Vector ep = e->PointGetNum(); + for(s = selection.First(); s; s = selection.NextAfter(s)) { + if(!s->entity.v) continue; + if(s->entity == stog->entity) + continue; + Entity *se = SK.GetEntity(s->entity); + if(!se->IsPoint()) continue; + if(ep.Equals(se->PointGetNum())) { + s->tag = 1; + } + } + } + } + selection.RemoveTagged(); +} + +//----------------------------------------------------------------------------- +// Select an item, if it isn't selected already. +//----------------------------------------------------------------------------- +void GraphicsWindow::MakeSelected(hEntity he) { + Selection stog = {}; + stog.entity = he; + MakeSelected(&stog); +} + +void GraphicsWindow::MakeSelected(hConstraint hc) { + Selection stog = {}; + stog.constraint = hc; + MakeSelected(&stog); +} + +void GraphicsWindow::MakeSelected(Selection *stog) { + if(stog->IsEmpty()) return; + if(IsSelected(stog)) return; + + if(stog->entity.v != 0 && SK.GetEntity(stog->entity)->IsFace()) { + // In the interest of speed for the triangle drawing code, + // only MAX_SELECTABLE_FACES faces may be selected at a time. + unsigned int c = 0; + Selection *s; + selection.ClearTags(); + for(s = selection.First(); s; s = selection.NextAfter(s)) { + hEntity he = s->entity; + if(he.v != 0 && SK.GetEntity(he)->IsFace()) { + c++; + // See also GraphicsWindow::GroupSelection "if(e->IsFace())" + // and Group::DrawMesh "case DrawMeshAs::SELECTED:" + if(c >= MAX_SELECTABLE_FACES) s->tag = 1; + } + } + selection.RemoveTagged(); + } + + selection.Add(stog); +} + +//----------------------------------------------------------------------------- +// Select everything that lies within the marquee view-aligned rectangle. +//----------------------------------------------------------------------------- +void GraphicsWindow::SelectByMarquee() { + Point2d marqueePoint = ProjectPoint(orig.marqueePoint); + BBox marqueeBBox = BBox::From(Vector::From(marqueePoint.x, marqueePoint.y, VERY_NEGATIVE), + Vector::From(orig.mouse.x, orig.mouse.y, VERY_POSITIVE)); + + for(Entity &e : SK.entity) { + if(e.group != SS.GW.activeGroup) continue; + if(e.IsFace() || e.IsDistance()) continue; + if(!e.IsVisible()) continue; + + bool entityHasBBox; + BBox entityBBox = e.GetOrGenerateScreenBBox(&entityHasBBox); + if(entityHasBBox && entityBBox.Overlaps(marqueeBBox)) { + if(e.type == Entity::Type::LINE_SEGMENT) { + Vector p0 = SS.GW.ProjectPoint3(e.EndpointStart()); + Vector p1 = SS.GW.ProjectPoint3(e.EndpointFinish()); + if((!marqueeBBox.Contains({p0.x, p0.y}, 0)) && + (!marqueeBBox.Contains({p1.x, p1.y}, 0))) { + // The selection marquee does not contain either of the line segment end points. + // This means that either the segment is entirely outside the marquee or that + // it intersects it. Check if it does... + if(!Vector::BoundingBoxIntersectsLine(marqueeBBox.maxp, marqueeBBox.minp, p0, + p1, true)) { + // ... it does not so it is outside. + continue; + } + } + } + MakeSelected(e.h); + } + } +} + +//----------------------------------------------------------------------------- +// Sort the selection according to various criteria: the entities and +// constraints separately, counts of certain types of entities (circles, +// lines, etc.), and so on. +//----------------------------------------------------------------------------- +void GraphicsWindow::GroupSelection() { + gs = {}; + int i; + for(i = 0; i < selection.n; i++) { + Selection *s = &(selection[i]); + if(s->entity.v) { + (gs.n)++; + + Entity *e = SK.entity.FindById(s->entity); + + if(e->IsStylable()) gs.stylables++; + + // A list of points, and a list of all entities that aren't points. + if(e->IsPoint()) { + gs.points++; + gs.point.push_back(s->entity); + } else { + gs.entities++; + gs.entity.push_back(s->entity); + } + + // And an auxiliary list of normals, including normals from + // workplanes. + if(e->IsNormal()) { + gs.anyNormals++; + gs.anyNormal.push_back(s->entity); + } else if(e->IsWorkplane()) { + gs.anyNormals++; + gs.anyNormal.push_back(e->Normal()->h); + } + + // And of vectors (i.e., stuff with a direction to constrain) + if(e->HasVector()) { + gs.vectors++; + gs.vector.push_back(s->entity); + } + + // Faces (which are special, associated/drawn with triangles) + if(e->IsFace()) { + gs.faces++; + gs.face.push_back(s->entity); + } + + if(e->HasEndpoints()) { + (gs.withEndpoints)++; + } + + // And some aux counts too + switch(e->type) { + case Entity::Type::WORKPLANE: (gs.workplanes)++; break; + case Entity::Type::LINE_SEGMENT: (gs.lineSegments)++; break; + case Entity::Type::CUBIC: (gs.cubics)++; break; + case Entity::Type::CUBIC_PERIODIC: (gs.periodicCubics)++; break; + + case Entity::Type::ARC_OF_CIRCLE: + (gs.circlesOrArcs)++; + (gs.arcs)++; + break; + + case Entity::Type::CIRCLE: (gs.circlesOrArcs)++; break; + + default: break; + } + } + if(s->constraint.v) { + gs.constraints++; + gs.constraint.push_back(s->constraint); + Constraint *c = SK.GetConstraint(s->constraint); + if(c->IsStylable()) gs.stylables++; + if(c->HasLabel()) gs.constraintLabels++; + } + } +} + +Camera GraphicsWindow::GetCamera() const { + Camera camera = {}; + if(window) { + window->GetContentSize(&camera.width, &camera.height); + camera.pixelRatio = window->GetDevicePixelRatio(); + camera.gridFit = (window->GetDevicePixelRatio() == 1); + } else { // solvespace-cli + camera.width = 297.0; // A4? Whatever... + camera.height = 210.0; + camera.pixelRatio = 1.0; + camera.gridFit = camera.pixelRatio == 1.0; + } + camera.offset = offset; + camera.projUp = projUp; + camera.projRight = projRight; + camera.scale = scale; + camera.tangent = SS.CameraTangent(); + return camera; +} + +Lighting GraphicsWindow::GetLighting() const { + Lighting lighting = {}; + lighting.backgroundColor = SS.backgroundColor; + lighting.ambientIntensity = SS.ambientIntensity; + lighting.lightIntensity[0] = SS.lightIntensity[0]; + lighting.lightIntensity[1] = SS.lightIntensity[1]; + lighting.lightDirection[0] = SS.lightDir[0]; + lighting.lightDirection[1] = SS.lightDir[1]; + return lighting; +} + +GraphicsWindow::Selection GraphicsWindow::ChooseFromHoverToSelect() { + Selection sel = {}; + if(hoverList.IsEmpty()) + return sel; + + Group *activeGroup = SK.GetGroup(SS.GW.activeGroup); + int bestOrder = -1; + int bestZIndex = 0; + double bestDepth = VERY_POSITIVE; + + for(const Hover &hov : hoverList) { + hGroup hg = {}; + if(hov.selection.entity.v != 0) { + hg = SK.GetEntity(hov.selection.entity)->group; + } else if(hov.selection.constraint.v != 0) { + hg = SK.GetConstraint(hov.selection.constraint)->group; + } + + Group *g = SK.GetGroup(hg); + if(g->order > activeGroup->order) continue; + if(bestOrder != -1 && (bestOrder > g->order || bestZIndex > hov.zIndex)) continue; + // we have hov.zIndex is >= best and hov.group is >= best (but not > active group) + if(hov.depth > bestDepth && bestOrder == g->order && bestZIndex == hov.zIndex) continue; + bestOrder = g->order; + bestZIndex = hov.zIndex; + bestDepth = hov.depth; + sel = hov.selection; + } + return sel; +} + +// This uses the same logic as hovering and static entity selection +// but ignores points known not to be draggable +GraphicsWindow::Selection GraphicsWindow::ChooseFromHoverToDrag() { + Selection sel = {}; + if(hoverList.IsEmpty()) + return sel; + + Group *activeGroup = SK.GetGroup(SS.GW.activeGroup); + int bestOrder = -1; + int bestZIndex = 0; + double bestDepth = VERY_POSITIVE; + + for(const Hover &hov : hoverList) { + hGroup hg = {}; + if(hov.selection.entity.v != 0) { + Entity *e = SK.GetEntity(hov.selection.entity); + if (!e->CanBeDragged()) continue; + hg = e->group; + } else if(hov.selection.constraint.v != 0) { + hg = SK.GetConstraint(hov.selection.constraint)->group; + } + + Group *g = SK.GetGroup(hg); + if(g->order > activeGroup->order) continue; + if(bestOrder != -1 && (bestOrder > g->order || bestZIndex > hov.zIndex)) continue; + // we have hov.zIndex is >= best and hov.group is >= best (but not > active group) + if(hov.depth > bestDepth && bestOrder == g->order && bestZIndex == hov.zIndex) continue; + bestOrder = g->order; + bestZIndex = hov.zIndex; + sel = hov.selection; + } + return sel; +} + +void GraphicsWindow::HitTestMakeSelection(Point2d mp) { + hoverList = {}; + Selection sel = {}; + + // Did the view projection change? If so, invalidate bounding boxes. + if(!offset.EqualsExactly(cached.offset) || + !projRight.EqualsExactly(cached.projRight) || + !projUp.EqualsExactly(cached.projUp) || + EXACT(scale != cached.scale)) { + cached.offset = offset; + cached.projRight = projRight; + cached.projUp = projUp; + cached.scale = scale; + for(Entity &e : SK.entity) { + e.screenBBoxValid = false; + } + } + + ObjectPicker canvas = {}; + canvas.camera = GetCamera(); + canvas.selRadius = 10.0; + canvas.point = mp; + canvas.maxZIndex = -1; + + // Always do the entities; we might be dragging something that should + // be auto-constrained, and we need the hover for that. + for(Entity &e : SK.entity) { + if(!e.IsVisible()) continue; + + // If faces aren't selectable, image entities aren't either. + if(e.type == Entity::Type::IMAGE && !showFaces) continue; + + // Don't hover whatever's being dragged. + if(IsFromPending(e.h.request())) { + // The one exception is when we're creating a new cubic; we + // want to be able to hover the first point, because that's + // how we turn it into a periodic spline. + if(!e.IsPoint()) continue; + if(!e.h.isFromRequest()) continue; + Request *r = SK.GetRequest(e.h.request()); + if(r->type != Request::Type::CUBIC) continue; + if(r->extraPoints < 2) continue; + if(e.h.v != r->h.entity(1).v) continue; + } + + if(canvas.Pick([&]{ e.Draw(Entity::DrawAs::DEFAULT, &canvas); })) { + Hover hov = {}; + hov.distance = canvas.minDistance; + hov.zIndex = canvas.maxZIndex; + hov.depth = canvas.minDepth; + hov.selection.entity = e.h; + hoverList.Add(&hov); + } + } + + // The constraints and faces happen only when nothing's in progress. + if(pending.operation == Pending::NONE) { + // Constraints + for(Constraint &c : SK.constraint) { + if(canvas.Pick([&]{ c.Draw(Constraint::DrawAs::DEFAULT, &canvas); })) { + Hover hov = {}; + hov.distance = canvas.minDistance; + hov.zIndex = canvas.maxZIndex; + hov.selection.constraint = c.h; + hoverList.Add(&hov); + } + } + } + + std::sort(hoverList.begin(), hoverList.end(), + [](const Hover &a, const Hover &b) { + if(a.zIndex == b.zIndex) return a.distance < b.distance; + return a.zIndex > b.zIndex; + }); + sel = ChooseFromHoverToSelect(); + + if(pending.operation == Pending::NONE) { + // Faces, from the triangle mesh; these are lowest priority + if(sel.constraint.v == 0 && sel.entity.v == 0 && showShaded && showFaces) { + Group *g = SK.GetGroup(activeGroup); + SMesh *m = &(g->displayMesh); + + uint32_t v = m->FirstIntersectionWith(mp); + if(v) { + sel.entity.v = v; + Hover hov = {}; + hov.selection = sel; + hoverList.Add(&hov); + } + } + } + + canvas.Clear(); + + if(!sel.Equals(&hover)) { + hover = sel; + Invalidate(); + } +} + +//----------------------------------------------------------------------------- +// Project a point in model space to screen space, exactly as gl would; return +// units are pixels. +//----------------------------------------------------------------------------- +Point2d GraphicsWindow::ProjectPoint(Vector p) { + Vector p3 = ProjectPoint3(p); + Point2d p2 = { p3.x, p3.y }; + return p2; +} +//----------------------------------------------------------------------------- +// Project a point in model space to screen space, exactly as gl would; return +// units are pixels. The z coordinate is also returned, also in pixels. +//----------------------------------------------------------------------------- +Vector GraphicsWindow::ProjectPoint3(Vector p) { + double w; + Vector r = ProjectPoint4(p, &w); + return r.ScaledBy(scale/w); +} +//----------------------------------------------------------------------------- +// Project a point in model space halfway into screen space. The scale is +// not applied, and the perspective divide isn't applied; instead the w +// coordinate is returned separately. +//----------------------------------------------------------------------------- +Vector GraphicsWindow::ProjectPoint4(Vector p, double *w) { + p = p.Plus(offset); + + Vector r; + r.x = p.Dot(projRight); + r.y = p.Dot(projUp); + r.z = p.Dot(projUp.Cross(projRight)); + + *w = 1 + r.z*SS.CameraTangent()*scale; + return r; +} + +//----------------------------------------------------------------------------- +// Return a point in the plane parallel to the screen and through the offset, +// that projects onto the specified (x, y) coordinates. +//----------------------------------------------------------------------------- +Vector GraphicsWindow::UnProjectPoint(Point2d p) { + Vector orig = offset.ScaledBy(-1); + + // Note that we ignoring the effects of perspective. Since our returned + // point has the same component normal to the screen as the offset, it + // will have z = 0 after the rotation is applied, thus w = 1. So this is + // correct. + orig = orig.Plus(projRight.ScaledBy(p.x / scale)).Plus( + projUp. ScaledBy(p.y / scale)); + return orig; +} + +Vector GraphicsWindow::UnProjectPoint3(Vector p) { + p.z = p.z / (scale - p.z * SS.CameraTangent() * scale); + double w = 1 + p.z * SS.CameraTangent() * scale; + p.x *= w / scale; + p.y *= w / scale; + + Vector orig = offset.ScaledBy(-1); + orig = orig.Plus(projRight.ScaledBy(p.x)).Plus( + projUp. ScaledBy(p.y).Plus( + projRight.Cross(projUp). ScaledBy(p.z))); + return orig; +} + +void GraphicsWindow::NormalizeProjectionVectors() { + if(projRight.Magnitude() < LENGTH_EPS) { + projRight = Vector::From(1, 0, 0); + } + + Vector norm = projRight.Cross(projUp); + // If projRight and projUp somehow ended up parallel, then pick an + // arbitrary projUp normal to projRight. + if(norm.Magnitude() < LENGTH_EPS) { + norm = projRight.Normal(0); + } + projUp = norm.Cross(projRight); + + projUp = projUp.WithMagnitude(1); + projRight = projRight.WithMagnitude(1); +} + +void GraphicsWindow::DrawSnapGrid(Canvas *canvas) { + if(!LockedInWorkplane()) return; + + const Camera &camera = canvas->GetCamera(); + double width = camera.width, + height = camera.height; + + hEntity he = ActiveWorkplane(); + EntityBase *wrkpl = SK.GetEntity(he), + *norm = wrkpl->Normal(); + Vector n = projUp.Cross(projRight); + Vector wu, wv, wn, wp; + wp = SK.GetEntity(wrkpl->point[0])->PointGetNum(); + wu = norm->NormalU(); + wv = norm->NormalV(); + wn = norm->NormalN(); + + double g = SS.gridSpacing; + + double umin = VERY_POSITIVE, umax = VERY_NEGATIVE, + vmin = VERY_POSITIVE, vmax = VERY_NEGATIVE; + int a; + for(a = 0; a < 4; a++) { + // Ideally, we would just do +/- half the width and height; but + // allow some extra slop for rounding. + Vector horiz = projRight.ScaledBy((0.6*width)/scale + 2*g), + vert = projUp. ScaledBy((0.6*height)/scale + 2*g); + if(a == 2 || a == 3) horiz = horiz.ScaledBy(-1); + if(a == 1 || a == 3) vert = vert. ScaledBy(-1); + Vector tp = horiz.Plus(vert).Minus(offset); + + // Project the point into our grid plane, normal to the screen + // (not to the grid plane). If the plane is on edge then this is + // impossible so don't try to draw the grid. + bool parallel; + Vector tpp = Vector::AtIntersectionOfPlaneAndLine( + wn, wn.Dot(wp), + tp, tp.Plus(n), + ¶llel); + if(parallel) return; + + tpp = tpp.Minus(wp); + double uu = tpp.Dot(wu), + vv = tpp.Dot(wv); + + umin = min(uu, umin); + umax = max(uu, umax); + vmin = min(vv, vmin); + vmax = max(vv, vmax); + } + + int i, j, i0, i1, j0, j1; + + i0 = (int)(umin / g); + i1 = (int)(umax / g); + j0 = (int)(vmin / g); + j1 = (int)(vmax / g); + + if(i0 > i1 || i1 - i0 > 400) return; + if(j0 > j1 || j1 - j0 > 400) return; + + Canvas::Stroke stroke = {}; + stroke.layer = Canvas::Layer::BACK; + stroke.color = Style::Color(Style::DATUM).WithAlpha(75); + stroke.unit = Canvas::Unit::PX; + stroke.width = 1.0f; + Canvas::hStroke hcs = canvas->GetStroke(stroke); + + for(i = i0 + 1; i < i1; i++) { + canvas->DrawLine(wp.Plus(wu.ScaledBy(i*g)).Plus(wv.ScaledBy(j0*g)), + wp.Plus(wu.ScaledBy(i*g)).Plus(wv.ScaledBy(j1*g)), + hcs); + } + for(j = j0 + 1; j < j1; j++) { + canvas->DrawLine(wp.Plus(wu.ScaledBy(i0*g)).Plus(wv.ScaledBy(j*g)), + wp.Plus(wu.ScaledBy(i1*g)).Plus(wv.ScaledBy(j*g)), + hcs); + } +} + +void GraphicsWindow::DrawEntities(Canvas *canvas, bool persistent) { + for(Entity &e : SK.entity) { + if(persistent == (e.IsNormal() || e.IsWorkplane())) continue; + switch(SS.GW.drawOccludedAs) { + case DrawOccludedAs::VISIBLE: + e.Draw(Entity::DrawAs::OVERLAY, canvas); + break; + + case DrawOccludedAs::STIPPLED: + e.Draw(Entity::DrawAs::HIDDEN, canvas); + /* fallthrough */ + case DrawOccludedAs::INVISIBLE: + e.Draw(Entity::DrawAs::DEFAULT, canvas); + break; + } + } +} + +void GraphicsWindow::DrawPersistent(Canvas *canvas) { + // Draw the active group; this does stuff like the mesh and edges. + SK.GetGroup(activeGroup)->Draw(canvas); + + // Now draw the entities that don't change with viewport. + DrawEntities(canvas, /*persistent=*/true); + + // Draw filled paths in all groups, when those filled paths were requested + // specially by assigning a style with a fill color, or when the filled + // paths are just being filled by default. This should go last, to make + // the transparency work. + for(hGroup hg : SK.groupOrder) { + Group *g = SK.GetGroup(hg); + if(!(g->IsVisible())) continue; + g->DrawFilledPaths(canvas); + } +} + +void GraphicsWindow::Draw(Canvas *canvas) { + const Camera &camera = canvas->GetCamera(); + + // Nasty case when we're reloading the linked files; could be that + // we get an error, so a dialog pops up, and a message loop starts, and + // we have to get called to paint ourselves. If the sketch is screwed + // up, then we could trigger an oops trying to draw. + if(!SS.allConsistent) return; + + if(showSnapGrid) DrawSnapGrid(canvas); + + // Draw all the things that don't change when we rotate. + if(persistentCanvas != NULL) { + if(persistentDirty) { + persistentDirty = false; + + persistentCanvas->Clear(); + DrawPersistent(&*persistentCanvas); + persistentCanvas->Finalize(); + } + + persistentCanvas->Draw(); + } else { + DrawPersistent(canvas); + } + + // Draw the entities that do change with viewport. + DrawEntities(canvas, /*persistent=*/false); + + // Draw the polygon errors. + if(SS.checkClosedContour) { + SK.GetGroup(activeGroup)->DrawPolyError(canvas); + } + + // Draw the constraints + for(Constraint &c : SK.constraint) { + c.Draw(Constraint::DrawAs::DEFAULT, canvas); + } + + // Draw areas + if(SS.showContourAreas) { + for(hGroup hg : SK.groupOrder) { + Group *g = SK.GetGroup(hg); + if(g->h != activeGroup) continue; + if(!(g->IsVisible())) continue; + g->DrawContourAreaLabels(canvas); + } + } + + // Draw the "pending" constraint, i.e. a constraint that would be + // placed on a line that is almost horizontal or vertical. + if(SS.GW.pending.operation == Pending::DRAGGING_NEW_LINE_POINT && + SS.GW.pending.hasSuggestion) { + Constraint c = {}; + c.group = SS.GW.activeGroup; + c.workplane = SS.GW.ActiveWorkplane(); + c.type = SS.GW.pending.suggestion; + c.entityA = SS.GW.pending.request.entity(0); + c.Draw(Constraint::DrawAs::DEFAULT, canvas); + } + + Canvas::Stroke strokeAnalyze = Style::Stroke(Style::ANALYZE); + strokeAnalyze.layer = Canvas::Layer::FRONT; + Canvas::hStroke hcsAnalyze = canvas->GetStroke(strokeAnalyze); + + // Draw the traced path, if one exists + SEdgeList tracedEdges = {}; + SS.traced.path.MakeEdgesInto(&tracedEdges); + canvas->DrawEdges(tracedEdges, hcsAnalyze); + tracedEdges.Clear(); + + Canvas::Stroke strokeError = Style::Stroke(Style::DRAW_ERROR); + strokeError.layer = Canvas::Layer::FRONT; + strokeError.width = 12; + Canvas::hStroke hcsError = canvas->GetStroke(strokeError); + + // And the naked edges, if the user did Analyze -> Show Naked Edges. + canvas->DrawEdges(SS.nakedEdges, hcsError); + + // Then redraw whatever the mouse is hovering over, highlighted. + hover.Draw(/*isHovered=*/true, canvas); + SK.GetGroup(activeGroup)->DrawMesh(Group::DrawMeshAs::HOVERED, canvas); + + // And finally draw the selection, same mechanism. + for(Selection *s = selection.First(); s; s = selection.NextAfter(s)) { + s->Draw(/*isHovered=*/false, canvas); + } + SK.GetGroup(activeGroup)->DrawMesh(Group::DrawMeshAs::SELECTED, canvas); + + Canvas::Stroke strokeDatum = Style::Stroke(Style::DATUM); + strokeDatum.unit = Canvas::Unit::PX; + strokeDatum.layer = Canvas::Layer::FRONT; + strokeDatum.width = 1; + Canvas::hStroke hcsDatum = canvas->GetStroke(strokeDatum); + + // An extra line, used to indicate the origin when rotating within the + // plane of the monitor. + if(SS.extraLine.draw) { + canvas->DrawLine(SS.extraLine.ptA, SS.extraLine.ptB, hcsDatum); + } + + if(SS.centerOfMass.draw && !SS.centerOfMass.dirty) { + Vector p = SS.centerOfMass.position; + Vector u = camera.projRight; + Vector v = camera.projUp; + + const double size = 10.0; + const int subdiv = 16; + double h = Style::DefaultTextHeight() / camera.scale; + std::string s = + SS.MmToStringSI(p.x) + ", " + + SS.MmToStringSI(p.y) + ", " + + SS.MmToStringSI(p.z); + canvas->DrawVectorText(s.c_str(), h, + p.Plus(u.ScaledBy((size + 5.0)/scale)).Minus(v.ScaledBy(h / 2.0)), + u, v, hcsDatum); + u = u.WithMagnitude(size / scale); + v = v.WithMagnitude(size / scale); + + canvas->DrawLine(p.Minus(u), p.Plus(u), hcsDatum); + canvas->DrawLine(p.Minus(v), p.Plus(v), hcsDatum); + Vector prev; + for(int i = 0; i <= subdiv; i++) { + double a = (double)i / subdiv * 2.0 * PI; + Vector point = p.Plus(u.ScaledBy(cos(a))).Plus(v.ScaledBy(sin(a))); + if(i > 0) { + canvas->DrawLine(point, prev, hcsDatum); + } + prev = point; + } + } + + // A note to indicate the origin in the just-exported file. + if(SS.justExportedInfo.draw) { + Vector p, u, v; + if(SS.justExportedInfo.showOrigin) { + p = SS.justExportedInfo.pt, + u = SS.justExportedInfo.u, + v = SS.justExportedInfo.v; + } else { + p = camera.offset.ScaledBy(-1); + u = camera.projRight; + v = camera.projUp; + } + canvas->DrawVectorText("previewing exported geometry; press Esc to return", + Style::DefaultTextHeight() / camera.scale, + p.Plus(u.ScaledBy(10/scale)).Plus(v.ScaledBy(10/scale)), u, v, + hcsDatum); + + if(SS.justExportedInfo.showOrigin) { + Vector um = p.Plus(u.WithMagnitude(-15/scale)), + up = p.Plus(u.WithMagnitude(30/scale)), + vm = p.Plus(v.WithMagnitude(-15/scale)), + vp = p.Plus(v.WithMagnitude(30/scale)); + canvas->DrawLine(um, up, hcsDatum); + canvas->DrawLine(vm, vp, hcsDatum); + canvas->DrawVectorText("(x, y) = (0, 0) for file just exported", + Style::DefaultTextHeight() / camera.scale, + p.Plus(u.ScaledBy(40/scale)).Plus( + v.ScaledBy(-(Style::DefaultTextHeight())/scale)), u, v, + hcsDatum); + } + } +} + +void GraphicsWindow::Paint() { + ssassert(window != NULL && canvas != NULL, + "Cannot paint without window and canvas"); + + havePainted = true; + + Camera camera = GetCamera(); + Lighting lighting = GetLighting(); + + if(!SS.ActiveGroupsOkay()) { + // Draw a different background whenever we're having solve problems. + RgbaColor bgColor = Style::Color(Style::DRAW_ERROR); + bgColor = RgbaColor::FromFloat(0.4f*bgColor.redF(), + 0.4f*bgColor.greenF(), + 0.4f*bgColor.blueF()); + lighting.backgroundColor = bgColor; + // And show the text window, which has info to debug it + ForceTextWindowShown(); + } + + canvas->SetLighting(lighting); + canvas->SetCamera(camera); + canvas->StartFrame(); + + // Draw the 3d objects. + Draw(canvas.get()); + canvas->FlushFrame(); + + // Draw the 2d UI overlay. + camera.LoadIdentity(); + camera.offset.x = -(double)camera.width / 2.0; + camera.offset.y = -(double)camera.height / 2.0; + canvas->SetCamera(camera); + + UiCanvas uiCanvas = {}; + uiCanvas.canvas = canvas; + + // If a marquee selection is in progress, then draw the selection + // rectangle, as an outline and a transparent fill. + if(pending.operation == Pending::DRAGGING_MARQUEE) { + Point2d begin = ProjectPoint(orig.marqueePoint); + uiCanvas.DrawRect((int)orig.mouse.x + (int)camera.width / 2, + (int)begin.x + (int)camera.width / 2, + (int)orig.mouse.y + (int)camera.height / 2, + (int)begin.y + (int)camera.height / 2, + /*fillColor=*/Style::Color(Style::HOVERED).WithAlpha(25), + /*outlineColor=*/Style::Color(Style::HOVERED)); + } + + // If we've had a screenshot requested, take it now, before the UI is overlaid. + if(!SS.screenshotFile.IsEmpty()) { + FILE *f = OpenFile(SS.screenshotFile, "wb"); + if(!f || !canvas->ReadFrame()->WritePng(f, /*flip=*/true)) { + Error("Couldn't write to '%s'", SS.screenshotFile.raw.c_str()); + } + if(f) fclose(f); + SS.screenshotFile.Clear(); + } + + // And finally the toolbar. + if(SS.showToolbar) { + canvas->SetCamera(camera); + ToolbarDraw(&uiCanvas); + } + + canvas->FlushFrame(); + canvas->FinishFrame(); + canvas->Clear(); +} + +void GraphicsWindow::Invalidate(bool clearPersistent) { + if(window) { + if(clearPersistent) { + persistentDirty = true; + } + window->Invalidate(); + } +} + +} // namespace SolveSpace diff --git a/src/drawconstraint.cpp b/src/drawconstraint.cpp new file mode 100644 index 0000000..bf7d389 --- /dev/null +++ b/src/drawconstraint.cpp @@ -0,0 +1,1384 @@ +//----------------------------------------------------------------------------- +// Given a constraint, draw a graphical and user-selectable representation +// of that constraint on-screen. We can either draw with gl, or compute the +// distance from a point (the location of the mouse pointer) to the lines +// that we would have drawn, for selection. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +std::string Constraint::Label() const { + std::string result; + if(type == Type::ANGLE) { + result = SS.DegreeToString(valA) + "°"; + } else if(type == Type::LENGTH_RATIO || type == Type::ARC_ARC_LEN_RATIO || type == Type::ARC_LINE_LEN_RATIO) { + result = ssprintf("%.3f:1", valA); + } else if(type == Type::COMMENT) { + result = comment; + } else if(type == Type::DIAMETER) { + if(!other) { + result = "⌀" + SS.MmToStringSI(valA); + } else { + result = "R" + SS.MmToStringSI(valA / 2); + } + } else { + // valA has units of distance + result = SS.MmToStringSI(fabs(valA)); + } + if(reference) { + result += " REF"; + } + return result; +} + +void Constraint::DoLine(Canvas *canvas, Canvas::hStroke hcs, Vector a, Vector b) { + const Camera &camera = canvas->GetCamera(); + + a = camera.AlignToPixelGrid(a); + b = camera.AlignToPixelGrid(b); + canvas->DrawLine(a, b, hcs); +} + +void Constraint::DoStippledLine(Canvas *canvas, Canvas::hStroke hcs, Vector a, Vector b) { + Canvas::Stroke strokeStippled = *canvas->strokes.FindById(hcs); + strokeStippled.stipplePattern = StipplePattern::SHORT_DASH; + strokeStippled.stippleScale = 4.0; + Canvas::hStroke hcsStippled = canvas->GetStroke(strokeStippled); + DoLine(canvas, hcsStippled, a, b); +} + +void Constraint::DoLabel(Canvas *canvas, Canvas::hStroke hcs, + Vector ref, Vector *labelPos, Vector gr, Vector gu) { + const Camera &camera = canvas->GetCamera(); + + std::string s = Label(); + double textHeight = Style::TextHeight(GetStyle()) / camera.scale; + double swidth = VectorFont::Builtin()->GetWidth(textHeight, s), + sheight = VectorFont::Builtin()->GetCapHeight(textHeight); + + // By default, the reference is from the center; but the style could + // specify otherwise if one is present, and it could also specify a + // rotation. + if(type == Type::COMMENT && disp.style.v) { + Style *st = Style::Get(disp.style); + // rotation first + double rads = st->textAngle*PI/180; + double c = cos(rads), s = sin(rads); + Vector pr = gr, pu = gu; + gr = pr.ScaledBy( c).Plus(pu.ScaledBy(s)); + gu = pr.ScaledBy(-s).Plus(pu.ScaledBy(c)); + // then origin + uint32_t o = (uint32_t)st->textOrigin; + if(o & (uint32_t)Style::TextOrigin::LEFT) ref = ref.Plus(gr.WithMagnitude(swidth/2)); + if(o & (uint32_t)Style::TextOrigin::RIGHT) ref = ref.Minus(gr.WithMagnitude(swidth/2)); + if(o & (uint32_t)Style::TextOrigin::BOT) ref = ref.Plus(gu.WithMagnitude(sheight/2)); + if(o & (uint32_t)Style::TextOrigin::TOP) ref = ref.Minus(gu.WithMagnitude(sheight/2)); + } + + Vector o = ref.Minus(gr.WithMagnitude(swidth/2)).Minus( + gu.WithMagnitude(sheight/2)); + canvas->DrawVectorText(s, textHeight, o, gr.WithMagnitude(1), gu.WithMagnitude(1), hcs); + if(labelPos) *labelPos = o; +} + +void Constraint::DoProjectedPoint(Canvas *canvas, Canvas::hStroke hcs, + Vector *r, Vector n, Vector o) { + double d = r->DistanceToPlane(n, o); + Vector p = r->Minus(n.ScaledBy(d)); + DoStippledLine(canvas, hcs, p, *r); + *r = p; +} + +void Constraint::DoProjectedPoint(Canvas *canvas, Canvas::hStroke hcs, Vector *r) { + Vector p = r->ProjectInto(workplane); + DoStippledLine(canvas, hcs, p, *r); + *r = p; +} + +//----------------------------------------------------------------------------- +// There is a rectangular box, aligned to our display axes (projRight, projUp) +// centered at ref. This is where a dimension label will be drawn. We want to +// draw a line from A to B. If that line would intersect the label box, then +// trim the line to leave a gap for it, and return zero. If not, then extend +// the line to almost meet the box, and return either positive or negative, +// depending whether that extension was from A or from B. +//----------------------------------------------------------------------------- +int Constraint::DoLineTrimmedAgainstBox(Canvas *canvas, Canvas::hStroke hcs, + Vector ref, Vector a, Vector b, bool extend) { + const Camera &camera = canvas->GetCamera(); + double th = Style::TextHeight(GetStyle()) / camera.scale; + double pixels = 1.0 / camera.scale; + double swidth = VectorFont::Builtin()->GetWidth(th, Label()) + 8 * pixels, + sheight = VectorFont::Builtin()->GetCapHeight(th) + 8 * pixels; + Vector gu = camera.projUp.WithMagnitude(1), + gr = camera.projRight.WithMagnitude(1); + return DoLineTrimmedAgainstBox(canvas, hcs, ref, a, b, extend, gr, gu, swidth, sheight); +} + +int Constraint::DoLineTrimmedAgainstBox(Canvas *canvas, Canvas::hStroke hcs, + Vector ref, Vector a, Vector b, bool extend, + Vector gr, Vector gu, double swidth, double sheight) { + struct { + Vector n; + double d; + } planes[4]; + // reference pos is the center of box occupied by text; build a rectangle + // around that, aligned to axes gr and gu, from four planes will all four + // normals pointing inward + planes[0].n = gu.ScaledBy(-1); planes[0].d = -(gu.Dot(ref) + sheight/2); + planes[1].n = gu; planes[1].d = gu.Dot(ref) - sheight/2; + planes[2].n = gr; planes[2].d = gr.Dot(ref) - swidth/2; + planes[3].n = gr.ScaledBy(-1); planes[3].d = -(gr.Dot(ref) + swidth/2); + + double tmin = VERY_POSITIVE, tmax = VERY_NEGATIVE; + Vector dl = b.Minus(a); + + for(int i = 0; i < 4; i++) { + bool parallel; + Vector p = Vector::AtIntersectionOfPlaneAndLine( + planes[i].n, planes[i].d, + a, b, ¶llel); + if(parallel) continue; + + int j; + for(j = 0; j < 4; j++) { + double d = (planes[j].n).Dot(p) - planes[j].d; + if(d < -LENGTH_EPS) break; + } + if(j < 4) continue; + + double t = (p.Minus(a)).DivProjected(dl); + tmin = min(t, tmin); + tmax = max(t, tmax); + } + + // Both in range; so there's pieces of the line on both sides of the label box. + if(tmin >= 0.0 && tmin <= 1.0 && tmax >= 0.0 && tmax <= 1.0) { + DoLine(canvas, hcs, a, a.Plus(dl.ScaledBy(tmin))); + DoLine(canvas, hcs, a.Plus(dl.ScaledBy(tmax)), b); + return 0; + } + + // Only one intersection in range; so the box is right on top of the endpoint + if(tmin >= 0.0 && tmin <= 1.0) { + DoLine(canvas, hcs, a, a.Plus(dl.ScaledBy(tmin))); + return 0; + } + + // Likewise. + if(tmax >= 0.0 && tmax <= 1.0) { + DoLine(canvas, hcs, a.Plus(dl.ScaledBy(tmax)), b); + return 0; + } + + // The line does not intersect the label; so the line should get + // extended to just barely meet the label. + // 0 means the label lies within the line, negative means it's outside + // and closer to b, positive means outside and closer to a. + if(tmax < 0.0) { + if(extend) a = a.Plus(dl.ScaledBy(tmax)); + DoLine(canvas, hcs, a, b); + return 1; + } + + if(tmin > 1.0) { + if(extend) b = a.Plus(dl.ScaledBy(tmin)); + DoLine(canvas, hcs, a, b); + return -1; + } + + // This will happen if the entire line lies within the box. + return 0; +} + +void Constraint::DoArrow(Canvas *canvas, Canvas::hStroke hcs, + Vector p, Vector dir, Vector n, double width, double angle, double da) { + dir = dir.WithMagnitude(width / cos(angle)); + dir = dir.RotatedAbout(n, da); + DoLine(canvas, hcs, p, p.Plus(dir.RotatedAbout(n, angle))); + DoLine(canvas, hcs, p, p.Plus(dir.RotatedAbout(n, -angle))); +} + +//----------------------------------------------------------------------------- +// Draw a line with arrows on both ends, and possibly a gap in the middle for +// the dimension. We will use these for most length dimensions. The length +// being dimensioned is from A to B; but those points get extended perpendicular +// to the line AB, until the line between the extensions crosses ref (the +// center of the label). +//----------------------------------------------------------------------------- +void Constraint::DoLineWithArrows(Canvas *canvas, Canvas::hStroke hcs, + Vector ref, Vector a, Vector b, + bool onlyOneExt) +{ + const Camera &camera = canvas->GetCamera(); + double pixels = 1.0 / camera.scale; + + Vector ab = a.Minus(b); + Vector ar = a.Minus(ref); + // Normal to a plane containing the line and the label origin. + Vector n = ab.Cross(ar); + // Within that plane, and normal to the line AB; so that's our extension + // line. + Vector out = ab.Cross(n).WithMagnitude(1); + out = out.ScaledBy(-out.Dot(ar)); + + Vector ae = a.Plus(out), be = b.Plus(out); + + // Extension lines extend 10 pixels beyond where the arrows get + // drawn (which is at the same offset perpendicular from AB as the + // label). + DoLine(canvas, hcs, a, ae.Plus(out.WithMagnitude(10*pixels))); + if(!onlyOneExt) { + DoLine(canvas, hcs, b, be.Plus(out.WithMagnitude(10*pixels))); + } + + int within = DoLineTrimmedAgainstBox(canvas, hcs, ref, ae, be); + + // Arrow heads are 13 pixels long, with an 18 degree half-angle. + double theta = 18*PI/180; + Vector arrow = (be.Minus(ae)).WithMagnitude(13*pixels); + + if(within != 0) { + arrow = arrow.ScaledBy(-1); + Vector seg = (be.Minus(ae)).WithMagnitude(18*pixels); + if(within < 0) DoLine(canvas, hcs, ae, ae.Minus(seg)); + if(within > 0) DoLine(canvas, hcs, be, be.Plus(seg)); + } + + DoArrow(canvas, hcs, ae, arrow, n, 13.0 * pixels, theta, 0.0); + DoArrow(canvas, hcs, be, arrow.Negated(), n, 13.0 * pixels, theta, 0.0); +} + +void Constraint::DoEqualLenTicks(Canvas *canvas, Canvas::hStroke hcs, + Vector a, Vector b, Vector gn, Vector *refp) { + const Camera &camera = canvas->GetCamera(); + + Vector m = (a.ScaledBy(1.0/3)).Plus(b.ScaledBy(2.0/3)); + if(refp) *refp = m; + Vector ab = a.Minus(b); + Vector n = (gn.Cross(ab)).WithMagnitude(10/camera.scale); + + DoLine(canvas, hcs, m.Minus(n), m.Plus(n)); +} + +void Constraint::DoEqualRadiusTicks(Canvas *canvas, Canvas::hStroke hcs, + hEntity he, Vector *refp) { + const Camera &camera = canvas->GetCamera(); + Entity *circ = SK.GetEntity(he); + + Vector center = SK.GetEntity(circ->point[0])->PointGetDrawNum(); + double r = circ->CircleGetRadiusNum(); + Quaternion q = circ->Normal()->NormalGetNum(); + Vector u = q.RotationU(), v = q.RotationV(); + + double theta; + if(circ->type == Entity::Type::CIRCLE) { + theta = PI/2; + } else if(circ->type == Entity::Type::ARC_OF_CIRCLE) { + double thetaa, thetab, dtheta; + circ->ArcGetAngles(&thetaa, &thetab, &dtheta); + theta = thetaa + dtheta/2; + } else ssassert(false, "Unexpected entity type"); + + Vector d = u.ScaledBy(cos(theta)).Plus(v.ScaledBy(sin(theta))); + d = d.ScaledBy(r); + Vector p = center.Plus(d); + if(refp) *refp = p; + Vector tick = d.WithMagnitude(10/camera.scale); + DoLine(canvas, hcs, p.Plus(tick), p.Minus(tick)); +} + +void Constraint::DoArcForAngle(Canvas *canvas, Canvas::hStroke hcs, + Vector a0, Vector da, Vector b0, Vector db, + Vector offset, Vector *ref, bool trim, + Vector explodeOffset) +{ + const Camera &camera = canvas->GetCamera(); + double pixels = 1.0 / camera.scale; + Vector gr = camera.projRight.ScaledBy(1.0); + Vector gu = camera.projUp.ScaledBy(1.0); + + if(workplane != Entity::FREE_IN_3D) { + a0 = a0.ProjectInto(workplane); + b0 = b0.ProjectInto(workplane); + da = da.ProjectVectorInto(workplane); + db = db.ProjectVectorInto(workplane); + } + + a0 = a0.Plus(explodeOffset); + b0 = b0.Plus(explodeOffset); + + Vector a1 = a0.Plus(da); + Vector b1 = b0.Plus(db); + + bool skew; + Vector pi = Vector::AtIntersectionOfLines(a0, a0.Plus(da), + b0, b0.Plus(db), &skew); + + if(!skew) { + *ref = pi.Plus(offset); + // We draw in a coordinate system centered at the intersection point. + // One basis vector is da, and the other is normal to da and in + // the plane that contains our lines (so normal to its normal). + da = da.WithMagnitude(1); + db = db.WithMagnitude(1); + + Vector norm = da.Cross(db); + + Vector dna = norm.Cross(da).WithMagnitude(1.0); + Vector dnb = norm.Cross(db).WithMagnitude(1.0); + + // da and db magnitudes are 1.0 + double thetaf = acos(da.Dot(db)); + + // Calculate median + Vector m = da.ScaledBy(cos(thetaf/2)).Plus( + dna.ScaledBy(sin(thetaf/2))); + Vector rm = (*ref).Minus(pi); + + // Test which side we have to place an arc + if(m.Dot(rm) < 0) { + da = da.ScaledBy(-1); dna = dna.ScaledBy(-1); + db = db.ScaledBy(-1); dnb = dnb.ScaledBy(-1); + } + + double rda = rm.Dot(da), rdna = rm.Dot(dna); + + // Introduce minimal arc radius in pixels + double r = max(sqrt(rda*rda + rdna*rdna), 15.0 * pixels); + + double th = Style::TextHeight(GetStyle()) / camera.scale; + double swidth = VectorFont::Builtin()->GetWidth(th, Label()) + 8*pixels, + sheight = VectorFont::Builtin()->GetCapHeight(th) + 6*pixels; + double textR = sqrt(swidth * swidth + sheight * sheight) / 2.0; + *ref = pi.Plus(rm.WithMagnitude(std::max(rm.Magnitude(), 15 * pixels + textR))); + + // Arrow points + Vector apa = da. ScaledBy(r).Plus(pi); + Vector apb = da. ScaledBy(r*cos(thetaf)).Plus( + dna.ScaledBy(r*sin(thetaf))).Plus(pi); + + double arrowW = 13 * pixels; + double arrowA = 18.0 * PI / 180.0; + bool arrowVisible = apb.Minus(apa).Magnitude() > 2.5 * arrowW; + // Arrow reversing indicator + bool arrowRev = false; + + // The minimal extension length in angular representation + double extAngle = 18 * pixels / r; + + // Arc additional angle + double addAngle = 0.0; + // Arc start angle + double startAngle = 0.0; + + // Arc extension to db. + // We have just enlarge angle value. + if(HasLabel() && rm.Dot(dnb) > 0.0) { + // rm direction projected to plane with u = da, v = dna + Vector rmp = da.ScaledBy(rda).Plus(dna.ScaledBy(rdna)).WithMagnitude(1.0); + // rmp and db magnitudes are 1.0 + addAngle = std::max(acos(rmp.Dot(db)), extAngle); + + if(arrowVisible) { + startAngle = -extAngle; + addAngle += extAngle; + arrowRev = true; + } + } + + // Arc extension to da. + // We are enlarge angle value and rewrite basis to align along rm projection. + if(HasLabel() && rm.Dot(dna) < 0.0) { + // rm direction projected to plane with u = da, v = dna + Vector rmp = da.ScaledBy(rda).Plus(dna.ScaledBy(rdna)).WithMagnitude(1.0); + // rmp and da magnitudes are 1.0 + startAngle = -std::max(acos(rmp.Dot(da)), extAngle); + addAngle = -startAngle; + + if(arrowVisible) { + addAngle += extAngle; + arrowRev = true; + } + } + + Vector prev; + int n = 30; + for(int i = 0; i <= n; i++) { + double theta = startAngle + (i*(thetaf + addAngle))/n; + Vector p = da.ScaledBy(r*cos(theta)).Plus( + dna.ScaledBy(r*sin(theta))).Plus(pi); + if(i > 0) { + if(trim) { + DoLineTrimmedAgainstBox(canvas, hcs, *ref, prev, p, + /*extend=*/false, gr, gu, swidth, sheight + 2*pixels); + } else { + DoLine(canvas, hcs, prev, p); + } + } + prev = p; + } + + DoLineExtend(canvas, hcs, a0, a1, apa, 5.0 * pixels); + DoLineExtend(canvas, hcs, b0, b1, apb, 5.0 * pixels); + + // Draw arrows only when we have enough space. + if(arrowVisible) { + double angleCorr = arrowW / (2.0 * r); + if(arrowRev) { + dna = dna.ScaledBy(-1.0); + angleCorr = -angleCorr; + } + DoArrow(canvas, hcs, apa, dna, norm, arrowW, arrowA, angleCorr); + DoArrow(canvas, hcs, apb, dna, norm, arrowW, arrowA, thetaf + PI - angleCorr); + } + } else { + // The lines are skew; no wonderful way to illustrate that. + + *ref = a0.Plus(b0); + *ref = (*ref).ScaledBy(0.5).Plus(disp.offset); + gu = gu.WithMagnitude(1); + double textHeight = Style::TextHeight(GetStyle()) / camera.scale; + Vector trans = + (*ref).Plus(gu.ScaledBy(-1.5*VectorFont::Builtin()->GetCapHeight(textHeight))); + canvas->DrawVectorText("angle between skew lines", textHeight, + trans, gr.WithMagnitude(1), gu.WithMagnitude(1), + hcs); + } +} + +bool Constraint::IsVisible() const { + if(SS.GW.showConstraints == GraphicsWindow::ShowConstraintMode::SCM_NOSHOW) + return false; + bool isDim = false; + + if(SS.GW.showConstraints == GraphicsWindow::ShowConstraintMode::SCM_SHOW_DIM) + switch(type) { + case ConstraintBase::Type::ANGLE: + case ConstraintBase::Type::DIAMETER: + case ConstraintBase::Type::PT_PT_DISTANCE: + case ConstraintBase::Type::PT_FACE_DISTANCE: + case ConstraintBase::Type::PT_LINE_DISTANCE: + case ConstraintBase::Type::PT_PLANE_DISTANCE: isDim = true; break; + default:; + } + + if(SS.GW.showConstraints == GraphicsWindow::ShowConstraintMode::SCM_SHOW_ALL || isDim ) { + Group *g = SK.GetGroup(group); + // If the group is hidden, then the constraints are hidden and not + // able to be selected. + if(!(g->visible)) return false; + // And likewise if the group is not the active group; except for comments + // with an assigned style. + if(g->h != SS.GW.activeGroup && !(type == Type::COMMENT && disp.style.v)) { + return false; + } + if(disp.style.v) { + Style *s = Style::Get(disp.style); + if(!s->visible) return false; + } + return true; + } + return false; +} + +bool Constraint::DoLineExtend(Canvas *canvas, Canvas::hStroke hcs, + Vector p0, Vector p1, Vector pt, double salient) { + Vector dir = p1.Minus(p0); + double k = dir.Dot(pt.Minus(p0)) / dir.Dot(dir); + Vector ptOnLine = p0.Plus(dir.ScaledBy(k)); + + // Draw projection line. + DoLine(canvas, hcs, pt, ptOnLine); + + // Calculate salient direction. + Vector sd = dir.WithMagnitude(1.0).ScaledBy(salient); + + Vector from; + Vector to; + + if(k < 0.0) { + from = p0; + to = ptOnLine.Minus(sd); + } else if(k > 1.0) { + from = p1; + to = ptOnLine.Plus(sd); + } else { + return false; + } + + // Draw extension line. + DoLine(canvas, hcs, from, to); + return true; +} + +void Constraint::DoLayout(DrawAs how, Canvas *canvas, + Vector *labelPos, std::vector *refs) { + if(!(how == DrawAs::HOVERED || how == DrawAs::SELECTED) && + !IsVisible()) return; + + // Unit vectors that describe our current view of the scene. One pixel + // long, not one actual unit. + const Camera &camera = canvas->GetCamera(); + Vector gr = camera.projRight.ScaledBy(1/camera.scale); + Vector gu = camera.projUp.ScaledBy(1/camera.scale); + Vector gn = (gr.Cross(gu)).WithMagnitude(1/camera.scale); + + double textHeight = Style::TextHeight(GetStyle()) / camera.scale; + + RgbaColor color = {}; + switch(how) { + case DrawAs::DEFAULT: color = Style::Color(GetStyle()); break; + case DrawAs::HOVERED: color = Style::Color(Style::HOVERED); break; + case DrawAs::SELECTED: color = Style::Color(Style::SELECTED); break; + } + Canvas::Stroke stroke = Style::Stroke(GetStyle()); + stroke.layer = Canvas::Layer::FRONT; + stroke.color = color; + stroke.zIndex = 4; + Canvas::hStroke hcs = canvas->GetStroke(stroke); + + Canvas::Fill fill = {}; + fill.layer = Canvas::Layer::FRONT; + fill.color = color; + fill.zIndex = stroke.zIndex; + Canvas::hFill hcf = canvas->GetFill(fill); + + switch(type) { + case Type::PT_PT_DISTANCE: { + Vector ap = SK.GetEntity(ptA)->PointGetNum(); + Vector bp = SK.GetEntity(ptB)->PointGetNum(); + + if(workplane != Entity::FREE_IN_3D) { + DoProjectedPoint(canvas, hcs, &ap); + DoProjectedPoint(canvas, hcs, &bp); + } + + if(ShouldDrawExploded()) { + // Offset A and B by the same offset so the constraint is drawn + // in the plane of one of the exploded points (rather than at an + // angle) + Vector offset = SK.GetEntity(ptA)->ExplodeOffset(); + ap = ap.Plus(offset); + bp = bp.Plus(offset); + } + + Vector ref = ((ap.Plus(bp)).ScaledBy(0.5)).Plus(disp.offset); + if(refs) refs->push_back(ref); + + DoLineWithArrows(canvas, hcs, ref, ap, bp, /*onlyOneExt=*/false); + DoLabel(canvas, hcs, ref, labelPos, gr, gu); + return; + } + + case Type::PROJ_PT_DISTANCE: { + Vector ap = SK.GetEntity(ptA)->PointGetNum(), + bp = SK.GetEntity(ptB)->PointGetNum(), + dp = (bp.Minus(ap)), + pp = SK.GetEntity(entityA)->VectorGetNum(); + + if(ShouldDrawExploded()) { + // explode for whichever point is in the workplane (or the first if both are) + Entity *pt = SK.GetEntity(ptA); + if(pt->group != group) { + pt = SK.GetEntity(ptB); + } + if(pt->group == group) { + Vector offset = pt->ExplodeOffset(); + ap = ap.Plus(offset); + bp = bp.Plus(offset); + } + } + + Vector ref = ((ap.Plus(bp)).ScaledBy(0.5)).Plus(disp.offset); + if(refs) refs->push_back(ref); + + pp = pp.WithMagnitude(1); + double d = dp.Dot(pp); + Vector bpp = ap.Plus(pp.ScaledBy(d)); + DoStippledLine(canvas, hcs, ap, bpp); + DoStippledLine(canvas, hcs, bp, bpp); + + DoLineWithArrows(canvas, hcs, ref, ap, bpp, /*onlyOneExt=*/false); + DoLabel(canvas, hcs, ref, labelPos, gr, gu); + return; + } + + case Type::PT_FACE_DISTANCE: + case Type::PT_PLANE_DISTANCE: { + Vector pt = SK.GetEntity(ptA)->PointGetDrawNum(); + Entity *enta = SK.GetEntity(entityA); + Vector n, p; + if(type == Type::PT_PLANE_DISTANCE) { + n = enta->Normal()->NormalN(); + p = enta->WorkplaneGetOffset(); + } else { + n = enta->FaceGetNormalNum(); + p = enta->FaceGetPointNum(); + } + + double d = (p.Minus(pt)).Dot(n); + Vector closest = pt.Plus(n.WithMagnitude(d)); + + Vector ref = ((closest.Plus(pt)).ScaledBy(0.5)).Plus(disp.offset); + if(refs) refs->push_back(ref); + + if(!pt.Equals(closest)) { + DoLineWithArrows(canvas, hcs, ref, pt, closest, /*onlyOneExt=*/true); + } + + DoLabel(canvas, hcs, ref, labelPos, gr, gu); + return; + } + + case Type::PT_LINE_DISTANCE: { + Entity *ptEntity = SK.GetEntity(ptA); + Vector pt = ptEntity->PointGetNum(); + Entity *line = SK.GetEntity(entityA); + Vector lA = SK.GetEntity(line->point[0])->PointGetNum(); + Vector lB = SK.GetEntity(line->point[1])->PointGetNum(); + Vector dl = lB.Minus(lA); + + if(workplane != Entity::FREE_IN_3D) { + lA = lA.ProjectInto(workplane); + lB = lB.ProjectInto(workplane); + DoProjectedPoint(canvas, hcs, &pt); + } + + // Only explode if the point and line are in the same group (and that group is a sketch + // with explode enabled) otherwise it's too visually confusing to figure out what the + // correct projections should be. + bool shouldExplode = ShouldDrawExploded() + && ptEntity->group == group + && line->group == group; + if(shouldExplode) { + Vector explodeOffset = ptEntity->ExplodeOffset(); + pt = pt.Plus(explodeOffset); + lA = lA.Plus(explodeOffset); + lB = lB.Plus(explodeOffset); + } + + // Find the closest point on the line + Vector closest = pt.ClosestPointOnLine(lA, dl); + + Vector ref = ((closest.Plus(pt)).ScaledBy(0.5)).Plus(disp.offset); + if(refs) refs->push_back(ref); + DoLabel(canvas, hcs, ref, labelPos, gr, gu); + + if(!pt.Equals(closest)) { + DoLineWithArrows(canvas, hcs, ref, pt, closest, /*onlyOneExt=*/true); + + // Draw projected point + Vector a = pt; + Vector b = closest; + Vector ab = a.Minus(b); + Vector ar = a.Minus(ref); + Vector n = ab.Cross(ar); + Vector out = ab.Cross(n).WithMagnitude(1); + out = out.ScaledBy(-out.Dot(ar)); + Vector be = b.Plus(out); + Vector np = lA.Minus(pt).Cross(lB.Minus(pt)).WithMagnitude(1.0); + DoProjectedPoint(canvas, hcs, &be, np, pt); + + // Extensions to line + double pixels = 1.0 / camera.scale; + Vector refClosest = ref.ClosestPointOnLine(lA, dl); + double ddl = dl.Dot(dl); + if(fabs(ddl) > LENGTH_EPS * LENGTH_EPS) { + double t = refClosest.Minus(lA).Dot(dl) / ddl; + if(t < 0.0) { + DoLine(canvas, hcs, refClosest.Minus(dl.WithMagnitude(10.0 * pixels)), lA); + } else if(t > 1.0) { + DoLine(canvas, hcs, refClosest.Plus(dl.WithMagnitude(10.0 * pixels)), lB); + } + } + } + + if(workplane != Entity::FREE_IN_3D) { + // Draw the projection marker from the closest point on the + // projected line to the projected point on the real line. + Vector lAB = (lA.Minus(lB)); + double t = (lA.Minus(closest)).DivProjected(lAB); + + Vector lA = SK.GetEntity(line->point[0])->PointGetNum(); + Vector lB = SK.GetEntity(line->point[1])->PointGetNum(); + + Vector c2 = (lA.ScaledBy(1-t)).Plus(lB.ScaledBy(t)); + DoProjectedPoint(canvas, hcs, &c2); + } + return; + } + + case Type::DIAMETER: { + Entity *circle = SK.GetEntity(entityA); + Vector center = SK.GetEntity(circle->point[0])->PointGetDrawNum(); + Quaternion q = SK.GetEntity(circle->normal)->NormalGetNum(); + Vector n = q.RotationN().WithMagnitude(1); + double r = circle->CircleGetRadiusNum(); + + Vector ref = center.Plus(disp.offset); + // Force the label into the same plane as the circle. + ref = ref.Minus(n.ScaledBy(n.Dot(ref) - n.Dot(center))); + if(refs) refs->push_back(ref); + + Vector mark = ref.Minus(center); + mark = mark.WithMagnitude(mark.Magnitude()-r); + DoLineTrimmedAgainstBox(canvas, hcs, ref, ref, ref.Minus(mark)); + + Vector topLeft; + DoLabel(canvas, hcs, ref, &topLeft, gr, gu); + if(labelPos) *labelPos = topLeft; + return; + } + + case Type::POINTS_COINCIDENT: { + if(how == DrawAs::DEFAULT) { + // Let's adjust the color of this constraint to have the same + // rough luma as the point color, so that the constraint does not + // stand out in an ugly way. + RgbaColor cd = Style::Color(Style::DATUM), + cc = Style::Color(Style::CONSTRAINT); + // convert from 8-bit color to a vector + Vector vd = Vector::From(cd.redF(), cd.greenF(), cd.blueF()), + vc = Vector::From(cc.redF(), cc.greenF(), cc.blueF()); + // and scale the constraint color to have the same magnitude as + // the datum color, maybe a bit dimmer + vc = vc.WithMagnitude(vd.Magnitude()*0.9); + // and set the color to that. + fill.color = RGBf(vc.x, vc.y, vc.z); + hcf = canvas->GetFill(fill); + } + + for(int a = 0; a < 2; a++) { + Vector r = camera.projRight.ScaledBy((a+1)/camera.scale); + Vector d = camera.projUp.ScaledBy((2-a)/camera.scale); + for(int i = 0; i < 2; i++) { + Vector p = SK.GetEntity(i == 0 ? ptA : ptB)->PointGetDrawNum(); + if(refs) refs->push_back(p); + canvas->DrawQuad(p.Plus (r).Plus (d), + p.Plus (r).Minus(d), + p.Minus(r).Minus(d), + p.Minus(r).Plus (d), + hcf); + } + + } + return; + } + + case Type::PT_ON_CIRCLE: + case Type::PT_ON_LINE: + case Type::PT_ON_FACE: + case Type::PT_IN_PLANE: { + double s = 8/camera.scale; + Vector p = SK.GetEntity(ptA)->PointGetDrawNum(); + if(refs) refs->push_back(p); + Vector r, d; + if(type == Type::PT_ON_FACE) { + Vector n = SK.GetEntity(entityA)->FaceGetNormalNum(); + r = n.Normal(0); + d = n.Normal(1); + } else if(type == Type::PT_IN_PLANE) { + EntityBase *n = SK.GetEntity(entityA)->Normal(); + r = n->NormalU(); + d = n->NormalV(); + } else { + r = gr; + d = gu; + s *= (6.0/8); // draw these a little smaller + } + r = r.WithMagnitude(s); d = d.WithMagnitude(s); + DoLine(canvas, hcs, p.Plus (r).Plus (d), p.Plus (r).Minus(d)); + DoLine(canvas, hcs, p.Plus (r).Minus(d), p.Minus(r).Minus(d)); + DoLine(canvas, hcs, p.Minus(r).Minus(d), p.Minus(r).Plus (d)); + DoLine(canvas, hcs, p.Minus(r).Plus (d), p.Plus (r).Plus (d)); + return; + } + + case Type::WHERE_DRAGGED: { + Vector p = SK.GetEntity(ptA)->PointGetDrawNum(); + if(refs) refs->push_back(p); + Vector u = p.Plus(gu.WithMagnitude(8/camera.scale)).Plus( + gr.WithMagnitude(8/camera.scale)), + uu = u.Minus(gu.WithMagnitude(5/camera.scale)), + ur = u.Minus(gr.WithMagnitude(5/camera.scale)); + // Draw four little crop marks, uniformly spaced (by ninety + // degree rotations) around the point. + int i; + for(i = 0; i < 4; i++) { + DoLine(canvas, hcs, u, uu); + DoLine(canvas, hcs, u, ur); + u = u.RotatedAbout(p, gn, PI/2); + ur = ur.RotatedAbout(p, gn, PI/2); + uu = uu.RotatedAbout(p, gn, PI/2); + } + return; + } + + case Type::SAME_ORIENTATION: { + for(int i = 0; i < 2; i++) { + Entity *e = SK.GetEntity(i == 0 ? entityA : entityB); + Quaternion q = e->NormalGetNum(); + Vector n = q.RotationN().WithMagnitude(25/camera.scale); + Vector u = q.RotationU().WithMagnitude(6/camera.scale); + Vector p = SK.GetEntity(e->point[0])->PointGetNum(); + p = p.Plus(n.WithMagnitude(10/camera.scale)); + if(refs) refs->push_back(p); + + DoLine(canvas, hcs, p.Plus(u), p.Minus(u).Plus(n)); + DoLine(canvas, hcs, p.Minus(u), p.Plus(u).Plus(n)); + } + return; + } + + case Type::EQUAL_ANGLE: { + Vector ref; + Entity *a = SK.GetEntity(entityA); + Entity *b = SK.GetEntity(entityB); + Entity *c = SK.GetEntity(entityC); + Entity *d = SK.GetEntity(entityD); + + Vector a0 = a->VectorGetStartPoint(); + Vector b0 = b->VectorGetStartPoint(); + Vector c0 = c->VectorGetStartPoint(); + Vector d0 = d->VectorGetStartPoint(); + Vector da = a->VectorGetNum(); + Vector db = b->VectorGetNum(); + Vector dc = c->VectorGetNum(); + Vector dd = d->VectorGetNum(); + + if(other) { + a0 = a0.Plus(da); + da = da.ScaledBy(-1); + } + + DoArcForAngle(canvas, hcs, a0, da, b0, db, + da.WithMagnitude(40/camera.scale), &ref, /*trim=*/false, a->ExplodeOffset()); + if(refs) refs->push_back(ref); + DoArcForAngle(canvas, hcs, c0, dc, d0, dd, + dc.WithMagnitude(40/camera.scale), &ref, /*trim=*/false, c->ExplodeOffset()); + if(refs) refs->push_back(ref); + + return; + } + + case Type::ANGLE: { + Entity *a = SK.GetEntity(entityA); + Entity *b = SK.GetEntity(entityB); + + Vector a0 = a->VectorGetStartPoint(); + Vector b0 = b->VectorGetStartPoint(); + Vector da = a->VectorGetNum(); + Vector db = b->VectorGetNum(); + if(other) { + a0 = a0.Plus(da); + da = da.ScaledBy(-1); + } + + Vector ref; + DoArcForAngle(canvas, hcs, a0, da, b0, db, disp.offset, &ref, /*trim=*/true, a->ExplodeOffset()); + DoLabel(canvas, hcs, ref, labelPos, gr, gu); + if(refs) refs->push_back(ref); + return; + } + + case Type::PERPENDICULAR: { + Vector u = Vector::From(0, 0, 0), v = Vector::From(0, 0, 0); + Vector rn, ru; + if(workplane == Entity::FREE_IN_3D) { + rn = gn; + ru = gu; + } else { + EntityBase *normal = SK.GetEntity(workplane)->Normal(); + rn = normal->NormalN(); + ru = normal->NormalV(); // ru meaning r_up, not u/v + } + + for(int i = 0; i < 2; i++) { + Entity *e = SK.GetEntity(i == 0 ? entityA : entityB); + + if(i == 0) { + // Calculate orientation of perpendicular sign only + // once, so that it's the same both times it's drawn + u = e->VectorGetNum(); + u = u.WithMagnitude(16/camera.scale); + v = (rn.Cross(u)).WithMagnitude(16/camera.scale); + // a bit of bias to stop it from flickering between the + // two possibilities + if(fabs(u.Dot(ru)) < fabs(v.Dot(ru)) + LENGTH_EPS) { + swap(u, v); + } + if(u.Dot(ru) < 0) u = u.ScaledBy(-1); + } + + Vector p = e->VectorGetRefPoint().Plus(e->ExplodeOffset()); + Vector s = p.Plus(u).Plus(v); + DoLine(canvas, hcs, s, s.Plus(v)); + Vector m = s.Plus(v.ScaledBy(0.5)); + DoLine(canvas, hcs, m, m.Plus(u)); + if(refs) refs->push_back(m); + } + return; + } + + case Type::CURVE_CURVE_TANGENT: + case Type::CUBIC_LINE_TANGENT: + case Type::ARC_LINE_TANGENT: { + Vector textAt, u, v; + + if(type == Type::ARC_LINE_TANGENT) { + Entity *arc = SK.GetEntity(entityA); + Entity *norm = SK.GetEntity(arc->normal); + Vector c = SK.GetEntity(arc->point[0])->PointGetDrawNum(); + Vector p = + SK.GetEntity(arc->point[other ? 2 : 1])->PointGetDrawNum(); + Vector r = p.Minus(c); + textAt = p.Plus(r.WithMagnitude(14/camera.scale)); + u = norm->NormalU(); + v = norm->NormalV(); + } else if(type == Type::CUBIC_LINE_TANGENT) { + Vector n; + if(workplane == Entity::FREE_IN_3D) { + u = gr; + v = gu; + n = gn; + } else { + EntityBase *wn = SK.GetEntity(workplane)->Normal(); + u = wn->NormalU(); + v = wn->NormalV(); + n = wn->NormalN(); + } + + Entity *cubic = SK.GetEntity(entityA); + Vector p = other ? cubic->CubicGetFinishNum() : + cubic->CubicGetStartNum(); + p = p.Plus(cubic->ExplodeOffset()); + Vector dir = SK.GetEntity(entityB)->VectorGetNum(); + Vector out = n.Cross(dir); + textAt = p.Plus(out.WithMagnitude(14/camera.scale)); + } else { + Vector n, dir; + EntityBase *wn = SK.GetEntity(workplane)->Normal(); + u = wn->NormalU(); + v = wn->NormalV(); + n = wn->NormalN(); + Entity *eA = SK.GetEntity(entityA); + // Big pain; we have to get a vector tangent to the curve + // at the shared point, which could be from either a cubic + // or an arc. + if(other) { + textAt = eA->EndpointFinish().Plus(eA->ExplodeOffset()); + if(eA->type == Entity::Type::CUBIC) { + dir = eA->CubicGetFinishTangentNum(); + } else { + dir = SK.GetEntity(eA->point[0])->PointGetNum().Minus( + SK.GetEntity(eA->point[2])->PointGetNum()); + dir = n.Cross(dir); + } + } else { + textAt = eA->EndpointStart().Plus(eA->ExplodeOffset()); + if(eA->type == Entity::Type::CUBIC) { + dir = eA->CubicGetStartTangentNum(); + } else { + dir = SK.GetEntity(eA->point[0])->PointGetNum().Minus( + SK.GetEntity(eA->point[1])->PointGetNum()); + dir = n.Cross(dir); + } + } + dir = n.Cross(dir); + textAt = textAt.Plus(dir.WithMagnitude(14/camera.scale)); + } + + Vector ex = VectorFont::Builtin()->GetExtents(textHeight, "T"); + canvas->DrawVectorText("T", textHeight, textAt.Minus(ex.ScaledBy(0.5)), + u.WithMagnitude(1), v.WithMagnitude(1), hcs); + if(refs) refs->push_back(textAt); + return; + } + + case Type::PARALLEL: { + for(int i = 0; i < 2; i++) { + Entity *e = SK.GetEntity(i == 0 ? entityA : entityB); + Vector n = e->VectorGetNum(); + n = n.WithMagnitude(25/camera.scale); + Vector u = (gn.Cross(n)).WithMagnitude(4/camera.scale); + Vector p = e->VectorGetRefPoint(); + + if(ShouldDrawExploded()) { + p = p.Plus(e->ExplodeOffset()); + } + + DoLine(canvas, hcs, p.Plus(u), p.Plus(u).Plus(n)); + DoLine(canvas, hcs, p.Minus(u), p.Minus(u).Plus(n)); + if(refs) refs->push_back(p.Plus(n.ScaledBy(0.5))); + } + return; + } + + case Type::EQUAL_RADIUS: { + for(int i = 0; i < 2; i++) { + Vector ref; + DoEqualRadiusTicks(canvas, hcs, i == 0 ? entityA : entityB, &ref); + if(refs) refs->push_back(ref); + } + return; + } + + case Type::EQUAL_LINE_ARC_LEN: { + Entity *line = SK.GetEntity(entityA); + Vector ref; + DoEqualLenTicks(canvas, hcs, + SK.GetEntity(line->point[0])->PointGetDrawNum(), + SK.GetEntity(line->point[1])->PointGetDrawNum(), + gn, &ref); + if(refs) refs->push_back(ref); + DoEqualRadiusTicks(canvas, hcs, entityB, &ref); + if(refs) refs->push_back(ref); + return; + } + + case Type::LENGTH_RATIO: + case Type::LENGTH_DIFFERENCE: + case Type::EQUAL_LENGTH_LINES: { + Vector a, b = Vector::From(0, 0, 0); + for(int i = 0; i < 2; i++) { + Entity *e = SK.GetEntity(i == 0 ? entityA : entityB); + a = SK.GetEntity(e->point[0])->PointGetNum(); + b = SK.GetEntity(e->point[1])->PointGetNum(); + + if(workplane != Entity::FREE_IN_3D) { + DoProjectedPoint(canvas, hcs, &a); + DoProjectedPoint(canvas, hcs, &b); + } + + if(ShouldDrawExploded()) { + Vector offset = e->ExplodeOffset(); + a = a.Plus(offset); + b = b.Plus(offset); + } + + Vector ref; + DoEqualLenTicks(canvas, hcs, a, b, gn, &ref); + if(refs) refs->push_back(ref); + } + if((type == Type::LENGTH_RATIO) || (type == Type::LENGTH_DIFFERENCE)) { + Vector ref = ((a.Plus(b)).ScaledBy(0.5)).Plus(disp.offset); + DoLabel(canvas, hcs, ref, labelPos, gr, gu); + } + return; + } + case Type::ARC_ARC_LEN_RATIO: + case Type::ARC_ARC_DIFFERENCE: { + Entity *circle = SK.GetEntity(entityA); + Vector center = SK.GetEntity(circle->point[0])->PointGetNum(); + Quaternion q = SK.GetEntity(circle->normal)->NormalGetNum(); + Vector n = q.RotationN().WithMagnitude(1); + + Vector ref2; + DoEqualRadiusTicks(canvas, hcs, entityA, &ref2); + DoEqualRadiusTicks(canvas, hcs, entityB, &ref2); + + Vector ref = center.Plus(disp.offset); + // Force the label into the same plane as the circle. + ref = ref.Minus(n.ScaledBy(n.Dot(ref) - n.Dot(center))); + if(refs) refs->push_back(ref); + Vector topLeft; + DoLabel(canvas, hcs, ref, &topLeft, gr, gu); + if(labelPos) *labelPos = topLeft; + return; + } + case Type::ARC_LINE_LEN_RATIO: + case Type::ARC_LINE_DIFFERENCE: { + Vector a, b = Vector::From(0, 0, 0); + Vector ref; + Entity *e = SK.GetEntity(entityA); + a = SK.GetEntity(e->point[0])->PointGetNum(); + b = SK.GetEntity(e->point[1])->PointGetNum(); + DoEqualLenTicks(canvas, hcs, a, b, gn, &ref); + if(refs) refs->push_back(ref); + DoEqualRadiusTicks(canvas, hcs, entityB, &ref); + if(refs) refs->push_back(ref); + ref = ((a.Plus(b)).ScaledBy(0.5)).Plus(disp.offset); + DoLabel(canvas, hcs, ref, labelPos, gr, gu); + return; + } + + case Type::EQ_LEN_PT_LINE_D: { + Entity *forLen = SK.GetEntity(entityA); + Vector a = SK.GetEntity(forLen->point[0])->PointGetNum(), + b = SK.GetEntity(forLen->point[1])->PointGetNum(); + if(workplane != Entity::FREE_IN_3D) { + DoProjectedPoint(canvas, hcs, &a); + DoProjectedPoint(canvas, hcs, &b); + } + if(ShouldDrawExploded()) { + Vector offset = forLen->ExplodeOffset(); + a = a.Plus(offset); + b = b.Plus(offset); + } + Vector refa; + DoEqualLenTicks(canvas, hcs, a, b, gn, &refa); + if(refs) refs->push_back(refa); + + Entity *ln = SK.GetEntity(entityB); + Vector la = SK.GetEntity(ln->point[0])->PointGetNum(), + lb = SK.GetEntity(ln->point[1])->PointGetNum(); + Vector pt = SK.GetEntity(ptA)->PointGetNum(); + if(workplane != Entity::FREE_IN_3D) { + DoProjectedPoint(canvas, hcs, &pt); + la = la.ProjectInto(workplane); + lb = lb.ProjectInto(workplane); + } + + Vector closest = pt.ClosestPointOnLine(la, lb.Minus(la)); + if(ShouldDrawExploded()) { + Vector offset = SK.GetEntity(ptA)->ExplodeOffset(); + pt = pt.Plus(offset); + closest = closest.Plus(offset); + } + DoLine(canvas, hcs, pt, closest); + Vector refb; + DoEqualLenTicks(canvas, hcs, pt, closest, gn, &refb); + if(refs) refs->push_back(refb); + return; + } + + case Type::EQ_PT_LN_DISTANCES: { + for(int i = 0; i < 2; i++) { + Entity *ln = SK.GetEntity(i == 0 ? entityA : entityB); + Vector la = SK.GetEntity(ln->point[0])->PointGetNum(), + lb = SK.GetEntity(ln->point[1])->PointGetNum(); + Entity *pte = SK.GetEntity(i == 0 ? ptA : ptB); + Vector pt = pte->PointGetNum(); + + if(workplane != Entity::FREE_IN_3D) { + DoProjectedPoint(canvas, hcs, &pt); + la = la.ProjectInto(workplane); + lb = lb.ProjectInto(workplane); + } + + Vector closest = pt.ClosestPointOnLine(la, lb.Minus(la)); + if(ShouldDrawExploded()) { + Vector offset = pte->ExplodeOffset(); + pt = pt.Plus(offset); + closest = closest.Plus(offset); + } + DoLine(canvas, hcs, pt, closest); + + Vector ref; + DoEqualLenTicks(canvas, hcs, pt, closest, gn, &ref); + if(refs) refs->push_back(ref); + } + return; + } + + { + case Type::SYMMETRIC: + Vector n; + n = SK.GetEntity(entityA)->Normal()->NormalN(); goto s; + case Type::SYMMETRIC_HORIZ: + n = SK.GetEntity(workplane)->Normal()->NormalU(); goto s; + case Type::SYMMETRIC_VERT: + n = SK.GetEntity(workplane)->Normal()->NormalV(); goto s; + case Type::SYMMETRIC_LINE: { + Entity *ln = SK.GetEntity(entityA); + Vector la = SK.GetEntity(ln->point[0])->PointGetNum(), + lb = SK.GetEntity(ln->point[1])->PointGetNum(); + la = la.ProjectInto(workplane); + lb = lb.ProjectInto(workplane); + n = lb.Minus(la); + Vector nw = SK.GetEntity(workplane)->Normal()->NormalN(); + n = n.RotatedAbout(nw, PI/2); + goto s; + } +s: + Vector a = SK.GetEntity(ptA)->PointGetDrawNum(); + Vector b = SK.GetEntity(ptB)->PointGetDrawNum(); + + for(int i = 0; i < 2; i++) { + Vector tail = (i == 0) ? a : b; + Vector d = (i == 0) ? b : a; + d = d.Minus(tail); + // Project the direction in which the arrow is drawn normal + // to the symmetry plane; for projected symmetry constraints, + // they might not be in the same direction, even when the + // constraint is fully solved. + d = n.ScaledBy(d.Dot(n)); + d = d.WithMagnitude(20/camera.scale); + Vector tip = tail.Plus(d); + + DoLine(canvas, hcs, tail, tip); + d = d.WithMagnitude(9/camera.scale); + DoLine(canvas, hcs, tip, tip.Minus(d.RotatedAbout(gn, 0.6))); + DoLine(canvas, hcs, tip, tip.Minus(d.RotatedAbout(gn, -0.6))); + if(refs) refs->push_back(tip); + } + return; + } + + case Type::AT_MIDPOINT: + case Type::HORIZONTAL: + case Type::VERTICAL: + if(entityA.v) { + Vector r, u, n; + if(workplane == Entity::FREE_IN_3D) { + r = gr; u = gu; n = gn; + } else { + r = SK.GetEntity(workplane)->Normal()->NormalU(); + u = SK.GetEntity(workplane)->Normal()->NormalV(); + n = r.Cross(u); + } + // For "at midpoint", this branch is always taken. + Entity *e = SK.GetEntity(entityA); + Vector a = SK.GetEntity(e->point[0])->PointGetDrawNum(); + Vector b = SK.GetEntity(e->point[1])->PointGetDrawNum(); + Vector m = (a.ScaledBy(0.5)).Plus(b.ScaledBy(0.5)); + Vector offset = (a.Minus(b)).Cross(n); + offset = offset.WithMagnitude(textHeight); + // Draw midpoint constraint on other side of line, so that + // a line can be midpoint and horizontal at same time. + if(type == Type::AT_MIDPOINT) offset = offset.ScaledBy(-1); + + std::string s; + switch(type) { + case Type::HORIZONTAL: s = "H"; break; + case Type::VERTICAL: s = "V"; break; + case Type::AT_MIDPOINT: s = "M"; break; + default: ssassert(false, "Unexpected constraint type"); + } + Vector o = m.Plus(offset).Plus(u.WithMagnitude(textHeight/5)), + ex = VectorFont::Builtin()->GetExtents(textHeight, s); + Vector shift = r.WithMagnitude(ex.x).Plus( + u.WithMagnitude(ex.y)); + + canvas->DrawVectorText(s, textHeight, o.Minus(shift.ScaledBy(0.5)), + r.WithMagnitude(1), u.WithMagnitude(1), hcs); + if(refs) refs->push_back(o); + } else { + Vector a = SK.GetEntity(ptA)->PointGetDrawNum(); + Vector b = SK.GetEntity(ptB)->PointGetDrawNum(); + + Entity *w = SK.GetEntity(workplane); + Vector cu = w->Normal()->NormalU(); + Vector cv = w->Normal()->NormalV(); + Vector cn = w->Normal()->NormalN(); + + int i; + for(i = 0; i < 2; i++) { + Vector o = (i == 0) ? a : b; + Vector oo = (i == 0) ? a.Minus(b) : b.Minus(a); + Vector d = (type == Type::HORIZONTAL) ? cu : cv; + if(oo.Dot(d) < 0) d = d.ScaledBy(-1); + + Vector dp = cn.Cross(d); + d = d.WithMagnitude(14/camera.scale); + Vector c = o.Minus(d); + DoLine(canvas, hcs, o, c); + d = d.WithMagnitude(3/camera.scale); + dp = dp.WithMagnitude(2/camera.scale); + canvas->DrawQuad((c.Plus(d)).Plus(dp), + (c.Minus(d)).Plus(dp), + (c.Minus(d)).Minus(dp), + (c.Plus(d)).Minus(dp), + hcf); + if(refs) refs->push_back(c); + } + } + return; + + case Type::COMMENT: { + Vector u, v; + if(workplane == Entity::FREE_IN_3D) { + u = gr; + v = gu; + } else { + EntityBase *norm = SK.GetEntity(workplane)->Normal(); + u = norm->NormalU(); + v = norm->NormalV(); + } + + if(disp.style.v != 0) { + RgbaColor color = stroke.color; + stroke = Style::Stroke(disp.style); + stroke.layer = Canvas::Layer::FRONT; + if(how != DrawAs::DEFAULT) { + stroke.color = color; + } + hcs = canvas->GetStroke(stroke); + } + Vector ref = disp.offset; + if(ptA.v) { + Vector a = SK.GetEntity(ptA)->PointGetNum(); + ref = a.Plus(disp.offset); + } + DoLabel(canvas, hcs, ref, labelPos, u, v); + if(refs) refs->push_back(ref); + return; + } + } + ssassert(false, "Unexpected constraint type"); +} + +void Constraint::Draw(DrawAs how, Canvas *canvas) { + DoLayout(how, canvas, NULL, NULL); +} + +Vector Constraint::GetLabelPos(const Camera &camera) { + Vector p; + + ObjectPicker canvas = {}; + canvas.camera = camera; + DoLayout(DrawAs::DEFAULT, &canvas, &p, NULL); + canvas.Clear(); + + return p; +} + +void Constraint::GetReferencePoints(const Camera &camera, std::vector *refs) { + ObjectPicker canvas = {}; + canvas.camera = camera; + DoLayout(DrawAs::DEFAULT, &canvas, NULL, refs); + canvas.Clear(); +} + +bool Constraint::IsStylable() const { + if(type == Type::COMMENT) return true; + return false; +} + +hStyle Constraint::GetStyle() const { + if(disp.style.v != 0) return disp.style; + return { Style::CONSTRAINT }; +} + +bool Constraint::HasLabel() const { + switch(type) { + case Type::COMMENT: + case Type::PT_PT_DISTANCE: + case Type::PT_PLANE_DISTANCE: + case Type::PT_LINE_DISTANCE: + case Type::PT_FACE_DISTANCE: + case Type::PROJ_PT_DISTANCE: + case Type::LENGTH_RATIO: + case Type::ARC_ARC_LEN_RATIO: + case Type::ARC_LINE_LEN_RATIO: + case Type::LENGTH_DIFFERENCE: + case Type::ARC_ARC_DIFFERENCE: + case Type::ARC_LINE_DIFFERENCE: + case Type::DIAMETER: + case Type::ANGLE: + return true; + + default: + return false; + } +} + +bool Constraint::ShouldDrawExploded() const { + return SK.GetGroup(group)->ShouldDrawExploded(); +} + +} // namespace SolveSpace diff --git a/src/drawentity.cpp b/src/drawentity.cpp new file mode 100644 index 0000000..bf856a1 --- /dev/null +++ b/src/drawentity.cpp @@ -0,0 +1,867 @@ +//----------------------------------------------------------------------------- +// Draw a representation of an entity on-screen, in the case of curves up +// to our chord tolerance, or return the distance from the user's mouse pointer +// to the entity for selection. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +std::string Entity::DescriptionString() const { + if(h.isFromRequest()) { + Request *r = SK.GetRequest(h.request()); + return r->DescriptionString(); + } else { + Group *g = SK.GetGroup(h.group()); + return g->DescriptionString(); + } +} + +void Entity::GenerateEdges(SEdgeList *el) { + SBezierList *sbl = GetOrGenerateBezierCurves(); + + for(int i = 0; i < sbl->l.n; i++) { + SBezier *sb = &(sbl->l[i]); + + List lv = {}; + sb->MakePwlInto(&lv); + for(int j = 1; j < lv.n; j++) { + el->AddEdge(lv[j-1], lv[j], Style::ForEntity(h).v, i); + } + lv.Clear(); + } +} + +SBezierList *Entity::GetOrGenerateBezierCurves() { + if(beziers.l.IsEmpty()) + GenerateBezierCurves(&beziers); + return &beziers; +} + +SEdgeList *Entity::GetOrGenerateEdges() { + if(!edges.l.IsEmpty()) { + if(EXACT(edgesChordTol == SS.ChordTolMm())) + return &edges; + edges.l.Clear(); + } + if(edges.l.IsEmpty()) + GenerateEdges(&edges); + edgesChordTol = SS.ChordTolMm(); + return &edges; +} + +BBox Entity::GetOrGenerateScreenBBox(bool *hasBBox) { + SBezierList *sbl = GetOrGenerateBezierCurves(); + + // We don't bother with bounding boxes for workplanes, etc. + *hasBBox = (IsPoint() || IsNormal() || !sbl->l.IsEmpty()); + if(!*hasBBox) return {}; + + if(screenBBoxValid) + return screenBBox; + + if(IsPoint()) { + Vector proj = SS.GW.ProjectPoint3(PointGetNum()); + screenBBox = BBox::From(proj, proj); + } else if(IsNormal()) { + Vector proj = SS.GW.ProjectPoint3(SK.GetEntity(point[0])->PointGetNum()); + screenBBox = BBox::From(proj, proj); + } else if(!sbl->l.IsEmpty()) { + Vector first = SS.GW.ProjectPoint3(sbl->l[0].ctrl[0]); + screenBBox = BBox::From(first, first); + for(auto &sb : sbl->l) { + for(int i = 0; i <= sb.deg; ++i) { screenBBox.Include(SS.GW.ProjectPoint3(sb.ctrl[i])); } + } + } else + ssassert(false, "Expected entity to be a point or have beziers"); + + screenBBoxValid = true; + return screenBBox; +} + +void Entity::GetReferencePoints(std::vector *refs) { + switch(type) { + case Type::POINT_N_COPY: + case Type::POINT_N_TRANS: + case Type::POINT_N_ROT_TRANS: + case Type::POINT_N_ROT_AA: + case Type::POINT_N_ROT_AXIS_TRANS: + case Type::POINT_IN_3D: + case Type::POINT_IN_2D: + refs->push_back(PointGetDrawNum()); + break; + + case Type::NORMAL_N_COPY: + case Type::NORMAL_N_ROT: + case Type::NORMAL_N_ROT_AA: + case Type::NORMAL_IN_3D: + case Type::NORMAL_IN_2D: + case Type::WORKPLANE: + case Type::CIRCLE: + case Type::ARC_OF_CIRCLE: + case Type::CUBIC: + case Type::CUBIC_PERIODIC: + case Type::TTF_TEXT: + case Type::IMAGE: + refs->push_back(SK.GetEntity(point[0])->PointGetDrawNum()); + break; + + case Type::LINE_SEGMENT: { + Vector a = SK.GetEntity(point[0])->PointGetDrawNum(), + b = SK.GetEntity(point[1])->PointGetDrawNum(); + refs->push_back(b.Plus(a.Minus(b).ScaledBy(0.5))); + break; + } + + case Type::DISTANCE: + case Type::DISTANCE_N_COPY: + case Type::FACE_NORMAL_PT: + case Type::FACE_XPROD: + case Type::FACE_N_ROT_TRANS: + case Type::FACE_N_TRANS: + case Type::FACE_N_ROT_AA: + case Type::FACE_ROT_NORMAL_PT: + case Type::FACE_N_ROT_AXIS_TRANS: + break; + } +} + +int Entity::GetPositionOfPoint(const Camera &camera, Point2d p) { + int position; + + ObjectPicker canvas = {}; + canvas.camera = camera; + canvas.point = p; + canvas.minDistance = 1e12; + Draw(DrawAs::DEFAULT, &canvas); + position = canvas.position; + canvas.Clear(); + + return position; +} + +bool Entity::IsStylable() const { + if(IsPoint()) return false; + if(IsWorkplane()) return false; + if(IsNormal()) return false; + return true; +} + +bool Entity::IsVisible() const { + Group *g = SK.GetGroup(group); + + if(g->h == Group::HGROUP_REFERENCES && IsNormal()) { + // The reference normals are always shown + return true; + } + if(!(g->IsVisible())) return false; + + if(IsPoint() && !SS.GW.showPoints) return false; + if(IsNormal() && !SS.GW.showNormals) return false; + if(construction && !SS.GW.showConstruction) return false; + + if(!SS.GW.showWorkplanes) { + if(IsWorkplane() && !h.isFromRequest()) { + if(g->h != SS.GW.activeGroup) { + // The group-associated workplanes are hidden outside + // their group. + return false; + } + } + } + + if(style.v) { + Style *s = Style::Get(style); + if(!s->visible) return false; + } + + if(forceHidden) return false; + + return true; +} + +static bool PtCanDrag(hEntity pt) { + Entity* p = SK.GetEntity(pt); + // a numeric copy can not move + if(p->type == Entity::Type::POINT_N_COPY) return false; + // these transforms applied zero times can not be moved + if(((p->type == Entity::Type::POINT_N_TRANS) || + (p->type == Entity::Type::POINT_N_ROT_AA) || + (p->type == Entity::Type::POINT_N_ROT_AXIS_TRANS)) + && (p->timesApplied == 0)) return false; + return true; +} + +// entities that were created via some copy types will not be +// draggable with the mouse. We identify the undraggables here +bool Entity::CanBeDragged() const { + if(IsPoint()) { + if(!PtCanDrag(h)) + return false; + // are we constrained pt-on-point from a previous group? + for(const Constraint &cc : SK.constraint) { + if(cc.group == group && cc.type == ConstraintBase::Type::POINTS_COINCIDENT) { + if(cc.ptA == h) { + if((SK.GetEntity(cc.ptB)->group < group) + || (!PtCanDrag(cc.ptB))) + return false; + } + if(cc.ptB == h) { + if((SK.GetEntity(cc.ptA)->group < group) + || (!PtCanDrag(cc.ptA))) + return false; + } + } + } + } + // for these types of entities the first point will indicate draggability + if(HasEndpoints() || type == Entity::Type::CIRCLE) { + return PtCanDrag(point[0]); + } + // if we're not certain it can't be dragged then default to true + return true; +} + +void Entity::CalculateNumerical(bool forExport) { + if(IsPoint()) actPoint = PointGetNum(); + if(IsNormal()) actNormal = NormalGetNum(); + if(type == Type::DISTANCE || type == Type::DISTANCE_N_COPY) { + actDistance = DistanceGetNum(); + } + if(IsFace()) { + actPoint = FaceGetPointNum(); + Vector n = FaceGetNormalNum(); + actNormal = Quaternion::From(0, n.x, n.y, n.z); + } + if(forExport) { + // Visibility in copied linked entities follows source file + actVisible = IsVisible(); + } else { + // Copied entities within a file are always visible + actVisible = true; + } +} + +//----------------------------------------------------------------------------- +// Compute a cubic, second derivative continuous, interpolating spline. Same +// routine for periodic splines (in a loop) or open splines (with specified +// end tangents). +//----------------------------------------------------------------------------- +void Entity::ComputeInterpolatingSpline(SBezierList *sbl, bool periodic) const { + static const int MAX_N = BandedMatrix::MAX_UNKNOWNS; + int ep = extraPoints; + + // The number of unknowns to solve for. + int n = periodic ? 3 + ep : ep; + ssassert(n < MAX_N, "Too many unknowns"); + // The number of on-curve points, one more than the number of segments. + int pts = periodic ? 4 + ep : 2 + ep; + + int i, j, a; + + // The starting and finishing control points that define our end tangents + // (if the spline isn't periodic), and the on-curve points. + Vector ctrl_s = Vector::From(0, 0, 0); + Vector ctrl_f = Vector::From(0, 0, 0); + Vector pt[MAX_N+4]; + if(periodic) { + for(i = 0; i < ep + 3; i++) { + pt[i] = SK.GetEntity(point[i])->PointGetNum(); + } + pt[i++] = SK.GetEntity(point[0])->PointGetNum(); + } else { + ctrl_s = SK.GetEntity(point[1])->PointGetNum(); + ctrl_f = SK.GetEntity(point[ep+2])->PointGetNum(); + j = 0; + pt[j++] = SK.GetEntity(point[0])->PointGetNum(); + for(i = 2; i <= ep + 1; i++) { + pt[j++] = SK.GetEntity(point[i])->PointGetNum(); + } + pt[j++] = SK.GetEntity(point[ep+3])->PointGetNum(); + } + + // The unknowns that we will be solving for, a set for each coordinate. + double Xx[MAX_N], Xy[MAX_N], Xz[MAX_N]; + // For a cubic Bezier section f(t) as t goes from 0 to 1, + // f' (0) = 3*(P1 - P0) + // f' (1) = 3*(P3 - P2) + // f''(0) = 6*(P0 - 2*P1 + P2) + // f''(1) = 6*(P3 - 2*P2 + P1) + for(a = 0; a < 3; a++) { + BandedMatrix bm = {}; + bm.n = n; + + for(i = 0; i < n; i++) { + int im, it, ip; + if(periodic) { + im = WRAP(i - 1, n); + it = i; + ip = WRAP(i + 1, n); + } else { + im = i; + it = i + 1; + ip = i + 2; + } + // All of these are expressed in terms of a constant part, and + // of X[i-1], X[i], and X[i+1]; so let these be the four + // components of that vector; + Vector4 A, B, C, D, E; + // The on-curve interpolated point + C = Vector4::From((pt[it]).Element(a), 0, 0, 0); + // control point one back, C - X[i] + B = C.Plus(Vector4::From(0, 0, -1, 0)); + // control point one forward, C + X[i] + D = C.Plus(Vector4::From(0, 0, 1, 0)); + // control point two back + if(i == 0 && !periodic) { + A = Vector4::From(ctrl_s.Element(a), 0, 0, 0); + } else { + // pt[im] + X[i-1] + A = Vector4::From(pt[im].Element(a), 1, 0, 0); + } + // control point two forward + if(i == (n - 1) && !periodic) { + E = Vector4::From(ctrl_f.Element(a), 0, 0, 0); + } else { + // pt[ip] - X[i+1] + E = Vector4::From((pt[ip]).Element(a), 0, 0, -1); + } + // Write the second derivatives of each segment, dropping constant + Vector4 fprev_pp = (C.Minus(B.ScaledBy(2))).Plus(A), + fnext_pp = (C.Minus(D.ScaledBy(2))).Plus(E), + eq = fprev_pp.Minus(fnext_pp); + + bm.B[i] = -eq.w; + if(periodic) { + bm.A[i][WRAP(i-2, n)] = eq.x; + bm.A[i][WRAP(i-1, n)] = eq.y; + bm.A[i][i] = eq.z; + } else { + // The wrapping would work, except when n = 1 and everything + // wraps to zero... + if(i > 0) { + bm.A[i][i - 1] = eq.x; + } + bm.A[i][i] = eq.y; + if(i < (n-1)) { + bm.A[i][i + 1] = eq.z; + } + } + } + bm.Solve(); + double *X = (a == 0) ? Xx : + (a == 1) ? Xy : + Xz; + memcpy(X, bm.X, n*sizeof(double)); + } + + for(i = 0; i < pts - 1; i++) { + Vector p0, p1, p2, p3; + if(periodic) { + p0 = pt[i]; + int iw = WRAP(i - 1, n); + p1 = p0.Plus(Vector::From(Xx[iw], Xy[iw], Xz[iw])); + } else if(i == 0) { + p0 = pt[0]; + p1 = ctrl_s; + } else { + p0 = pt[i]; + p1 = p0.Plus(Vector::From(Xx[i-1], Xy[i-1], Xz[i-1])); + } + if(periodic) { + p3 = pt[i+1]; + int iw = WRAP(i, n); + p2 = p3.Minus(Vector::From(Xx[iw], Xy[iw], Xz[iw])); + } else if(i == (pts - 2)) { + p3 = pt[pts-1]; + p2 = ctrl_f; + } else { + p3 = pt[i+1]; + p2 = p3.Minus(Vector::From(Xx[i], Xy[i], Xz[i])); + } + SBezier sb = SBezier::From(p0, p1, p2, p3); + sbl->l.Add(&sb); + } +} + +void Entity::GenerateBezierCurves(SBezierList *sbl) const { + SBezier sb; + + int i = sbl->l.n; + + switch(type) { + case Type::LINE_SEGMENT: { + Vector a = SK.GetEntity(point[0])->PointGetNum(); + Vector b = SK.GetEntity(point[1])->PointGetNum(); + sb = SBezier::From(a, b); + sb.entity = h.v; + sbl->l.Add(&sb); + break; + } + case Type::CUBIC: + ComputeInterpolatingSpline(sbl, /*periodic=*/false); + break; + + case Type::CUBIC_PERIODIC: + ComputeInterpolatingSpline(sbl, /*periodic=*/true); + break; + + case Type::CIRCLE: + case Type::ARC_OF_CIRCLE: { + Vector center = SK.GetEntity(point[0])->PointGetNum(); + Quaternion q = SK.GetEntity(normal)->NormalGetNum(); + Vector u = q.RotationU(), v = q.RotationV(); + double r = CircleGetRadiusNum(); + double thetaa, thetab, dtheta; + + if(r < LENGTH_EPS) { + // If a circle or an arc gets dragged through zero radius, + // then we just don't generate anything. + break; + } + + if(type == Type::CIRCLE) { + thetaa = 0; + thetab = 2*PI; + dtheta = 2*PI; + } else { + ArcGetAngles(&thetaa, &thetab, &dtheta); + } + int i, n; + if(dtheta > (3*PI/2 + 0.01)) { + n = 4; + } else if(dtheta > (PI + 0.01)) { + n = 3; + } else if(dtheta > (PI/2 + 0.01)) { + n = 2; + } else { + n = 1; + } + dtheta /= n; + + for(i = 0; i < n; i++) { + double s, c; + + c = cos(thetaa); + s = sin(thetaa); + // The start point of the curve, and the tangent vector at + // that start point. + Vector p0 = center.Plus(u.ScaledBy( r*c)).Plus(v.ScaledBy(r*s)), + t0 = u.ScaledBy(-r*s). Plus(v.ScaledBy(r*c)); + + thetaa += dtheta; + + c = cos(thetaa); + s = sin(thetaa); + Vector p2 = center.Plus(u.ScaledBy( r*c)).Plus(v.ScaledBy(r*s)), + t2 = u.ScaledBy(-r*s). Plus(v.ScaledBy(r*c)); + + // The control point must lie on both tangents. + Vector p1 = Vector::AtIntersectionOfLines(p0, p0.Plus(t0), + p2, p2.Plus(t2), + NULL); + + SBezier sb = SBezier::From(p0, p1, p2); + sb.weight[1] = cos(dtheta/2); + sbl->l.Add(&sb); + } + break; + } + + case Type::TTF_TEXT: { + Vector topLeft = SK.GetEntity(point[0])->PointGetNum(); + Vector botLeft = SK.GetEntity(point[1])->PointGetNum(); + Vector n = Normal()->NormalN(); + Vector v = topLeft.Minus(botLeft); + Vector u = (v.Cross(n)).WithMagnitude(v.Magnitude()); + + // `extraPoints` is storing kerning boolean + SS.fonts.PlotString(font, str, sbl, extraPoints, botLeft, u, v); + break; + } + + default: + // Not a problem, points and normals and such don't generate curves + break; + } + + // Record our style for all of the Beziers that we just created. + for(; i < sbl->l.n; i++) { + sbl->l[i].auxA = Style::ForEntity(h).v; + } +} + +bool Entity::ShouldDrawExploded() const { + return SK.GetGroup(group)->ShouldDrawExploded(); +} + +Vector Entity::ExplodeOffset() const { + if(ShouldDrawExploded() && workplane.v != 0) { + int requestIdx = SK.GetRequest(h.request())->groupRequestIndex; + double offset = SS.explodeDistance * (requestIdx + 1); + return SK.GetEntity(workplane)->Normal()->NormalN().ScaledBy(offset); + } else { + return Vector::From(0, 0, 0); + } +} + +Vector Entity::PointGetDrawNum() const { + // As per EntityBase::PointGetNum but specifically for when drawing/rendering the point + // (and not when solving), so we can potentially draw it somewhere different + return PointGetNum().Plus(ExplodeOffset()); +} + +void Entity::Draw(DrawAs how, Canvas *canvas) { + if(!(how == DrawAs::HOVERED || how == DrawAs::SELECTED) && + !IsVisible()) return; + + int zIndex; + if(IsPoint()) { + zIndex = 6; + } else if(how == DrawAs::HIDDEN) { + zIndex = 2; + } else if(group != SS.GW.activeGroup) { + zIndex = 3; + } else { + zIndex = 5; + } + + hStyle hs; + if(IsPoint()) { + hs.v = Style::DATUM; + } else if(IsNormal() || type == Type::WORKPLANE) { + hs.v = Style::NORMALS; + } else { + hs = Style::ForEntity(h); + if (hs.v == Style::CONSTRUCTION) { + zIndex = 4; + } + } + + Canvas::Stroke stroke = Style::Stroke(hs); + switch(how) { + case DrawAs::DEFAULT: + stroke.layer = Canvas::Layer::NORMAL; + break; + + case DrawAs::OVERLAY: + stroke.layer = Canvas::Layer::FRONT; + break; + + case DrawAs::HIDDEN: + stroke.layer = Canvas::Layer::OCCLUDED; + stroke.stipplePattern = Style::PatternType({ Style::HIDDEN_EDGE }); + stroke.stippleScale = Style::Get({ Style::HIDDEN_EDGE })->stippleScale; + break; + + case DrawAs::HOVERED: + stroke.layer = Canvas::Layer::FRONT; + stroke.color = Style::Color(Style::HOVERED); + break; + + case DrawAs::SELECTED: + stroke.layer = Canvas::Layer::FRONT; + stroke.color = Style::Color(Style::SELECTED); + break; + } + stroke.zIndex = zIndex; + Canvas::hStroke hcs = canvas->GetStroke(stroke); + + switch(type) { + case Type::POINT_N_COPY: + case Type::POINT_N_TRANS: + case Type::POINT_N_ROT_TRANS: + case Type::POINT_N_ROT_AA: + case Type::POINT_N_ROT_AXIS_TRANS: + case Type::POINT_IN_3D: + case Type::POINT_IN_2D: { + if(how == DrawAs::HIDDEN) return; + + // If we're analyzing the sketch to show the degrees of freedom, + // then we draw big colored squares over the points that are + // free to move. + bool free = false; + if(type == Type::POINT_IN_3D) { + Param *px = SK.GetParam(param[0]), + *py = SK.GetParam(param[1]), + *pz = SK.GetParam(param[2]); + + free = px->free || py->free || pz->free; + } else if(type == Type::POINT_IN_2D) { + Param *pu = SK.GetParam(param[0]), + *pv = SK.GetParam(param[1]); + + free = pu->free || pv->free; + } + + Canvas::Stroke pointStroke = {}; + pointStroke.layer = (free) ? Canvas::Layer::FRONT : stroke.layer; + pointStroke.zIndex = stroke.zIndex; + pointStroke.color = stroke.color; + pointStroke.width = 7.0; + pointStroke.unit = Canvas::Unit::PX; + Canvas::hStroke hcsPoint = canvas->GetStroke(pointStroke); + + Vector p = PointGetDrawNum(); + if(free) { + Canvas::Stroke analyzeStroke = Style::Stroke(Style::ANALYZE); + analyzeStroke.width = 14.0; + analyzeStroke.layer = Canvas::Layer::FRONT; + Canvas::hStroke hcsAnalyze = canvas->GetStroke(analyzeStroke); + + canvas->DrawPoint(p, hcsAnalyze); + } + + canvas->DrawPoint(p, hcsPoint); + return; + } + + case Type::NORMAL_N_COPY: + case Type::NORMAL_N_ROT: + case Type::NORMAL_N_ROT_AA: + case Type::NORMAL_IN_3D: + case Type::NORMAL_IN_2D: { + const Camera &camera = canvas->GetCamera(); + + if(how == DrawAs::HIDDEN) return; + + for(int i = 0; i < 2; i++) { + bool asReference = (i == 1); + if(asReference) { + if(!h.request().IsFromReferences()) continue; + } else { + if(!SK.GetGroup(group)->IsVisible() || !SS.GW.showNormals) continue; + } + + stroke.layer = (asReference) ? Canvas::Layer::FRONT : Canvas::Layer::NORMAL; + if(how != DrawAs::HOVERED && how != DrawAs::SELECTED) { + // Always draw the x, y, and z axes in red, green, and blue; + // brighter for the ones at the bottom left of the screen, + // dimmer for the ones at the model origin. + hRequest hr = h.request(); + uint8_t luma = (asReference) ? 255 : 100; + if(hr == Request::HREQUEST_REFERENCE_XY) { + stroke.color = RgbaColor::From(0, 0, luma); + } else if(hr == Request::HREQUEST_REFERENCE_YZ) { + stroke.color = RgbaColor::From(luma, 0, 0); + } else if(hr == Request::HREQUEST_REFERENCE_ZX) { + stroke.color = RgbaColor::From(0, luma, 0); + } + } + + Quaternion q = NormalGetNum(); + Vector tail; + if(asReference) { + // Draw an extra copy of the x, y, and z axes, that's + // always in the corner of the view and at the front. + // So those are always available, perhaps useful. + stroke.width = 2; + double s = camera.scale; + double h = 60 - camera.height / 2.0; + double w = 60 - camera.width / 2.0; + // Shift the axis to the right if they would overlap with the toolbar. + if(SS.showToolbar) { + if(h + 30 > -(32*18 + 3*16 + 8) / 2) + w += 60; + } + tail = camera.projRight.ScaledBy(w/s).Plus( + camera.projUp. ScaledBy(h/s)).Minus(camera.offset); + } else { + tail = SK.GetEntity(point[0])->PointGetDrawNum(); + } + tail = camera.AlignToPixelGrid(tail); + + hcs = canvas->GetStroke(stroke); + Vector v = (q.RotationN()).WithMagnitude(50.0 / camera.scale); + Vector tip = tail.Plus(v); + canvas->DrawLine(tail, tip, hcs); + + v = v.WithMagnitude(12.0 / camera.scale); + Vector axis = q.RotationV(); + canvas->DrawLine(tip, tip.Minus(v.RotatedAbout(axis, 0.6)), hcs); + canvas->DrawLine(tip, tip.Minus(v.RotatedAbout(axis, -0.6)), hcs); + + if(type == Type::NORMAL_IN_3D) { + Param *nw = SK.GetParam(param[0]), + *nx = SK.GetParam(param[1]), + *ny = SK.GetParam(param[2]), + *nz = SK.GetParam(param[3]); + + if(nw->free || nx->free || ny->free || nz->free) { + Canvas::Stroke analyzeStroke = Style::Stroke(Style::ANALYZE); + analyzeStroke.layer = Canvas::Layer::FRONT; + Canvas::hStroke hcsAnalyze = canvas->GetStroke(analyzeStroke); + canvas->DrawLine(tail, tip, hcsAnalyze); + } + } + } + return; + } + + case Type::DISTANCE: + case Type::DISTANCE_N_COPY: + // These are used only as data structures, nothing to display. + return; + + case Type::WORKPLANE: { + const Camera &camera = canvas->GetCamera(); + + Vector p = SK.GetEntity(point[0])->PointGetNum(); + p = camera.AlignToPixelGrid(p); + + Vector u = Normal()->NormalU(); + Vector v = Normal()->NormalV(); + + double s = (std::min(camera.width, camera.height)) * 0.45 / camera.scale; + + Vector us = u.ScaledBy(s); + Vector vs = v.ScaledBy(s); + + Vector pp = p.Plus (us).Plus (vs); + Vector pm = p.Plus (us).Minus(vs); + Vector mm = p.Minus(us).Minus(vs), mm2 = mm; + Vector mp = p.Minus(us).Plus (vs); + + Canvas::Stroke strokeBorder = stroke; + strokeBorder.zIndex -= 3; + strokeBorder.stipplePattern = StipplePattern::SHORT_DASH; + strokeBorder.stippleScale = 8.0; + Canvas::hStroke hcsBorder = canvas->GetStroke(strokeBorder); + + double textHeight = Style::TextHeight(hs) / camera.scale; + + if(!h.isFromRequest()) { + mm = mm.Plus(v.ScaledBy(textHeight * 4.7)); + mm2 = mm2.Plus(u.ScaledBy(textHeight * 4.7)); + canvas->DrawLine(mm2, mm, hcsBorder); + } + canvas->DrawLine(pp, pm, hcsBorder); + canvas->DrawLine(mm2, pm, hcsBorder); + canvas->DrawLine(mm, mp, hcsBorder); + canvas->DrawLine(pp, mp, hcsBorder); + + Vector o = mm2.Plus(u.ScaledBy(3.0 / camera.scale)).Plus( + v.ScaledBy(3.0 / camera.scale)); + std::string shortDesc = DescriptionString().substr(5); + canvas->DrawVectorText(shortDesc, textHeight, o, u, v, hcs); + return; + } + + case Type::LINE_SEGMENT: + case Type::CIRCLE: + case Type::ARC_OF_CIRCLE: + case Type::CUBIC: + case Type::CUBIC_PERIODIC: + case Type::TTF_TEXT: { + // Generate the rational polynomial curves, then piecewise linearize + // them, and display those. + // Calculating the draw offset, if necessary. + const bool shouldExplode = ShouldDrawExploded(); + Vector explodeOffset; + SBezierList offsetBeziers = {}; + SBezierList *beziers = GetOrGenerateBezierCurves(); + if(shouldExplode) { + explodeOffset = ExplodeOffset(); + for(const SBezier& b : beziers->l) { + SBezier offset = b.TransformedBy(explodeOffset, Quaternion::IDENTITY, 1.0); + offsetBeziers.l.Add(&offset); + } + beziers = &offsetBeziers; + } + + SEdgeList *edges = nullptr; + SEdgeList offsetEdges = {}; + + if(!canvas->DrawBeziers(*beziers, hcs)) { + edges = GetOrGenerateEdges(); + if(shouldExplode) { + for(const SEdge &e : edges->l) { + offsetEdges.AddEdge(e.a.Plus(explodeOffset), e.b.Plus(explodeOffset), e.auxA, e.auxB, e.tag); + } + edges = &offsetEdges; + } + canvas->DrawEdges(*edges, hcs); + } + if(type == Type::CIRCLE) { + Entity *dist = SK.GetEntity(distance); + if(dist->type == Type::DISTANCE) { + Param *p = SK.GetParam(dist->param[0]); + if(p->free) { + Canvas::Stroke analyzeStroke = Style::Stroke(Style::ANALYZE); + analyzeStroke.layer = Canvas::Layer::FRONT; + Canvas::hStroke hcsAnalyze = canvas->GetStroke(analyzeStroke); + if(!canvas->DrawBeziers(*beziers, hcsAnalyze)) { + canvas->DrawEdges(*edges, hcsAnalyze); + } + } + } + } + offsetBeziers.Clear(); + offsetEdges.Clear(); + return; + } + case Type::IMAGE: { + Canvas::Fill fill = {}; + std::shared_ptr pixmap; + switch(how) { + case DrawAs::HIDDEN: return; + + case DrawAs::HOVERED: { + fill.color = Style::Color(Style::HOVERED).WithAlpha(180); + fill.pattern = Canvas::FillPattern::CHECKERED_A; + fill.zIndex = 2; + break; + } + + case DrawAs::SELECTED: { + fill.color = Style::Color(Style::SELECTED).WithAlpha(180); + fill.pattern = Canvas::FillPattern::CHECKERED_B; + fill.zIndex = 1; + break; + } + + default: + fill.color = RgbaColor::FromFloat(1.0f, 1.0f, 1.0f); + pixmap = SS.images[file]; + break; + } + + Canvas::hFill hf = canvas->GetFill(fill); + Vector v[4] = {}; + for(int i = 0; i < 4; i++) { + v[i] = SK.GetEntity(point[i])->PointGetDrawNum(); + } + Vector iu = v[3].Minus(v[0]); + Vector iv = v[1].Minus(v[0]); + + if(how == DrawAs::DEFAULT && pixmap == NULL) { + Canvas::Stroke stroke = Style::Stroke(Style::DRAW_ERROR); + stroke.color = stroke.color.WithAlpha(50); + Canvas::hStroke hs = canvas->GetStroke(stroke); + canvas->DrawLine(v[0], v[2], hs); + canvas->DrawLine(v[1], v[3], hs); + for(int i = 0; i < 4; i++) { + canvas->DrawLine(v[i], v[(i + 1) % 4], hs); + } + } else { + canvas->DrawPixmap(pixmap, v[0], iu, iv, + Point2d::From(0.0, 0.0), Point2d::From(1.0, 1.0), hf); + } + } + + case Type::FACE_NORMAL_PT: + case Type::FACE_XPROD: + case Type::FACE_N_ROT_TRANS: + case Type::FACE_N_TRANS: + case Type::FACE_N_ROT_AA: + case Type::FACE_ROT_NORMAL_PT: + case Type::FACE_N_ROT_AXIS_TRANS: + // Do nothing; these are drawn with the triangle mesh + return; + } + ssassert(false, "Unexpected entity type"); +} + +} // namespace SolveSpace diff --git a/src/dsc.h b/src/dsc.h new file mode 100644 index 0000000..1a535c6 --- /dev/null +++ b/src/dsc.h @@ -0,0 +1,730 @@ +//----------------------------------------------------------------------------- +// Data structures used frequently in the program, various kinds of vectors +// (of real numbers, not symbolic algebra stuff) and our templated lists. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#ifndef SOLVESPACE_DSC_H +#define SOLVESPACE_DSC_H + +#include +#include +#include +#include +#include + +#include "defs.h" +#include "util.h" + +namespace SolveSpace { + +class Vector; +class Vector4; +class Point2d; +class hEntity; +class hParam; + +class Quaternion { +public: + // a + (vx)*i + (vy)*j + (vz)*k + double w, vx, vy, vz; + + static const Quaternion IDENTITY; + + static Quaternion From(double w, double vx, double vy, double vz); + static Quaternion From(hParam w, hParam vx, hParam vy, hParam vz); + static Quaternion From(Vector u, Vector v); + static Quaternion From(Vector axis, double dtheta); + + Quaternion Plus(Quaternion b) const; + Quaternion Minus(Quaternion b) const; + Quaternion ScaledBy(double s) const; + double Magnitude() const; + Quaternion WithMagnitude(double s) const; + + // Call a rotation matrix [ u' v' n' ]'; this returns the first and + // second rows, where that matrix is generated by this quaternion + Vector RotationU() const; + Vector RotationV() const; + Vector RotationN() const; + Vector Rotate(Vector p) const; + + Quaternion ToThe(double p) const; + Quaternion Inverse() const; + Quaternion Times(Quaternion b) const; + Quaternion Mirror() const; +}; + +class Vector { +public: + double x, y, z; + + static Vector From(double x, double y, double z); + static Vector From(hParam x, hParam y, hParam z); + static Vector AtIntersectionOfPlanes(Vector n1, double d1, + Vector n2, double d2); + static Vector AtIntersectionOfLines(Vector a0, Vector a1, + Vector b0, Vector b1, + bool *skew, + double *pa=NULL, double *pb=NULL); + static Vector AtIntersectionOfPlaneAndLine(Vector n, double d, + Vector p0, Vector p1, + bool *parallel); + static Vector AtIntersectionOfPlanes(Vector na, double da, + Vector nb, double db, + Vector nc, double dc, bool *parallel); + static void ClosestPointBetweenLines(Vector pa, Vector da, + Vector pb, Vector db, + double *ta, double *tb); + + double Element(int i) const; + bool Equals(Vector v, double tol=LENGTH_EPS) const; + bool EqualsExactly(Vector v) const; + Vector Plus(Vector b) const; + Vector Minus(Vector b) const; + Vector Negated() const; + Vector Cross(Vector b) const; + double DirectionCosineWith(Vector b) const; + double Dot(Vector b) const; + Vector Normal(int which) const; + Vector RotatedAbout(Vector orig, Vector axis, double theta) const; + Vector RotatedAbout(Vector axis, double theta) const; + Vector DotInToCsys(Vector u, Vector v, Vector n) const; + Vector ScaleOutOfCsys(Vector u, Vector v, Vector n) const; + double DistanceToLine(Vector p0, Vector dp) const; + double DistanceToPlane(Vector normal, Vector origin) const; + bool OnLineSegment(Vector a, Vector b, double tol=LENGTH_EPS) const; + Vector ClosestPointOnLine(Vector p0, Vector deltal) const; + double Magnitude() const; + double MagSquared() const; + Vector WithMagnitude(double s) const; + Vector ScaledBy(double s) const; + Vector ProjectInto(hEntity wrkpl) const; + Vector ProjectVectorInto(hEntity wrkpl) const; + double DivProjected(Vector delta) const; + Vector ClosestOrtho() const; + void MakeMaxMin(Vector *maxv, Vector *minv) const; + Vector ClampWithin(double minv, double maxv) const; + static bool BoundingBoxesDisjoint(Vector amax, Vector amin, + Vector bmax, Vector bmin); + static bool BoundingBoxIntersectsLine(Vector amax, Vector amin, + Vector p0, Vector p1, bool asSegment); + bool OutsideAndNotOn(Vector maxv, Vector minv) const; + Vector InPerspective(Vector u, Vector v, Vector n, + Vector origin, double cameraTan) const; + Point2d Project2d(Vector u, Vector v) const; + Point2d ProjectXy() const; + Vector4 Project4d() const; +}; + +inline double Vector::Element(int i) const { + switch (i) { + case 0: return x; + case 1: return y; + case 2: return z; + default: ssassert(false, "Unexpected vector element index"); + } +} + +inline bool Vector::Equals(Vector v, double tol) const { + // Quick axis-aligned tests before going further + const Vector dv = this->Minus(v); + if (std::abs(dv.x) > tol) return false; + if (std::abs(dv.y) > tol) return false; + if (std::abs(dv.z) > tol) return false; + + return dv.MagSquared() < tol*tol; +} + +inline Vector Vector::From(double x, double y, double z) { + return {x, y, z}; +} + +inline Vector Vector::Plus(Vector b) const { + return {x + b.x, y + b.y, z + b.z}; +} + +inline Vector Vector::Minus(Vector b) const { + return {x - b.x, y - b.y, z - b.z}; +} + +inline Vector Vector::Negated() const { + return {-x, -y, -z}; +} + +inline Vector Vector::Cross(Vector b) const { + return {-(z * b.y) + (y * b.z), (z * b.x) - (x * b.z), -(y * b.x) + (x * b.y)}; +} + +inline double Vector::Dot(Vector b) const { + return (x * b.x + y * b.y + z * b.z); +} + +inline double Vector::MagSquared() const { + return x * x + y * y + z * z; +} + +inline double Vector::Magnitude() const { + return sqrt(x * x + y * y + z * z); +} + +inline Vector Vector::ScaledBy(const double v) const { + return {x * v, y * v, z * v}; +} + +inline void Vector::MakeMaxMin(Vector *maxv, Vector *minv) const { + maxv->x = std::max(maxv->x, x); + maxv->y = std::max(maxv->y, y); + maxv->z = std::max(maxv->z, z); + + minv->x = std::min(minv->x, x); + minv->y = std::min(minv->y, y); + minv->z = std::min(minv->z, z); +} + +struct VectorHash { + size_t operator()(const Vector &v) const; +}; + +struct VectorPred { + bool operator()(Vector a, Vector b) const; +}; + +class Vector4 { +public: + double w, x, y, z; + + static Vector4 From(double w, double x, double y, double z); + static Vector4 From(double w, Vector v3); + static Vector4 Blend(Vector4 a, Vector4 b, double t); + + Vector4 Plus(Vector4 b) const; + Vector4 Minus(Vector4 b) const; + Vector4 ScaledBy(double s) const; + Vector PerspectiveProject() const; +}; + +class Point2d { +public: + double x, y; + + static Point2d From(double x, double y); + static Point2d FromPolar(double r, double a); + + Point2d Plus(const Point2d &b) const; + Point2d Minus(const Point2d &b) const; + Point2d ScaledBy(double s) const; + double DivProjected(Point2d delta) const; + double Dot(Point2d p) const; + double DistanceTo(const Point2d &p) const; + double DistanceToLine(const Point2d &p0, const Point2d &dp, bool asSegment) const; + double DistanceToLineSigned(const Point2d &p0, const Point2d &dp, bool asSegment) const; + double Angle() const; + double AngleTo(const Point2d &p) const; + double Magnitude() const; + double MagSquared() const; + Point2d WithMagnitude(double v) const; + Point2d Normal() const; + bool Equals(Point2d v, double tol=LENGTH_EPS) const; +}; + +// A simple list +template +class List { + T *elem = nullptr; + int elemsAllocated = 0; + +public: + int n = 0; + + bool IsEmpty() const { return n == 0; } + + void ReserveMore(int howMuch) { + if(n + howMuch > elemsAllocated) { + elemsAllocated = n + howMuch; + T *newElem = (T *)::operator new[]((size_t)elemsAllocated*sizeof(T)); + for(int i = 0; i < n; i++) { + new(&newElem[i]) T(std::move(elem[i])); + elem[i].~T(); + } + ::operator delete[](elem); + elem = newElem; + } + } + + void AllocForOneMore() { + if(n >= elemsAllocated) { + ReserveMore((elemsAllocated + 32)*2 - n); + } + } + + void Add(const T *t) { + AllocForOneMore(); + new(&elem[n++]) T(*t); + } + + void AddToBeginning(const T *t) { + AllocForOneMore(); + new(&elem[n]) T(); + std::move_backward(elem, elem + 1, elem + n + 1); + elem[0] = *t; + n++; + } + + T *First() { + return IsEmpty() ? nullptr : &(elem[0]); + } + const T *First() const { + return IsEmpty() ? nullptr : &(elem[0]); + } + + T *Last() { return IsEmpty() ? nullptr : &(elem[n - 1]); } + const T *Last() const { return IsEmpty() ? nullptr : &(elem[n - 1]); } + + T *NextAfter(T *prev) { + if(IsEmpty() || !prev) return NULL; + if(prev - First() == (n - 1)) return NULL; + return prev + 1; + } + const T *NextAfter(const T *prev) const { + if(IsEmpty() || !prev) return NULL; + if(prev - First() == (n - 1)) return NULL; + return prev + 1; + } + + T &Get(size_t i) { return elem[i]; } + T const &Get(size_t i) const { return elem[i]; } + T &operator[](size_t i) { return Get(i); } + T const &operator[](size_t i) const { return Get(i); } + + T *begin() { return IsEmpty() ? nullptr : &elem[0]; } + T *end() { return IsEmpty() ? nullptr : &elem[n]; } + const T *begin() const { return IsEmpty() ? nullptr : &elem[0]; } + const T *end() const { return IsEmpty() ? nullptr : &elem[n]; } + const T *cbegin() const { return begin(); } + const T *cend() const { return end(); } + + void ClearTags() { + for(auto & elt : *this) { + elt.tag = 0; + } + } + + void Clear() { + for(int i = 0; i < n; i++) + elem[i].~T(); + if(elem) ::operator delete[](elem); + elem = NULL; + n = elemsAllocated = 0; + } + + void RemoveTagged() { + auto newEnd = std::remove_if(this->begin(), this->end(), [](T &t) { + if(t.tag) { + return true; + } + return false; + }); + auto oldEnd = this->end(); + n = newEnd - begin(); + if (newEnd != nullptr && oldEnd != nullptr) { + while(newEnd != oldEnd) { + newEnd->~T(); + ++newEnd; + } + } + // and elemsAllocated is untouched, because we didn't resize + } + + void RemoveLast(int cnt) { + ssassert(n >= cnt, "Removing more elements than the list contains"); + for(int i = n - cnt; i < n; i++) + elem[i].~T(); + n -= cnt; + // and elemsAllocated is untouched, same as in RemoveTagged + } + + void Reverse() { + int i; + for(i = 0; i < (n/2); i++) { + swap(elem[i], elem[(n-1)-i]); + } + } +}; + +template class IdList; + +// Comparison functor used by IdList and related classes +template +struct CompareId { + + CompareId(const IdList *list) { + idlist = list; + } + + bool operator()(int lhs, T const& rhs) const { + return idlist->elemstore[lhs].h.v < rhs.h.v; + } + bool operator()(int lhs, H rhs) const { + return idlist->elemstore[lhs].h.v < rhs.v; + } + bool operator()(T *lhs, int rhs) const { + return lhs->h.v < idlist->elemstore[rhs].h.v; + } + +private: + const IdList *idlist; +}; + +// A list, where each element has an integer identifier. The list is kept +// sorted by that identifier, and items can be looked up in log n time by +// id. +template +class IdList { + std::vector elemstore; + std::vector elemidx; + std::vector freelist; +public: + int n = 0; // PAR@@@@@ make this private to see all interesting and suspicious places in SoveSpace ;-) + + friend struct CompareId; + using Compare = CompareId; + + struct iterator { + typedef std::random_access_iterator_tag iterator_category; + typedef T value_type; + typedef int difference_type; + typedef T *pointer; + typedef T &reference; + + public: + T &operator*() const noexcept { return *elem; } + const T *operator->() const noexcept { return elem; } + + bool operator==(const iterator &p) const { return p.position == position; } + bool operator!=(const iterator &p) const { return !operator==(p); } + + iterator &operator++() { + ++position; + if(position >= (int)list->elemidx.size()) { + elem = nullptr; // PAR@@@@ Remove just debugging + } else if(0 <= position) { + elem = &(list->elemstore[list->elemidx[position]]); + } + return *this; + } + + // Needed for std:find_if of gcc used in entity.cpp GenerateEquations + difference_type operator-(const iterator &rhs) const noexcept { + return position - rhs.position; + } + + iterator(IdList *l) : position(0), list(l) { + if(list) { + if(list->elemstore.size() && list->elemidx.size()) { + elem = &(list->elemstore[list->elemidx[position]]); + } + } + }; + iterator(IdList *l, int pos) : position(pos), list(l) { + if(position >= (int)list->elemidx.size()) { + elem = nullptr; + } else if(0 <= position) { + elem = &((list->elemstore)[list->elemidx[position]]); + } + }; + + private: + int position; + T *elem; + IdList *list; + }; + + + bool IsEmpty() const { + return n == 0; + } + + uint32_t MaximumId() { + if(IsEmpty()) { + return 0; + } else { + return elemstore[elemidx.back()].h.v; + } + } + + H AddAndAssignId(T *t) { + t->h.v = (MaximumId() + 1); + + // Add at the end of the list. + elemstore.push_back(*t); + elemidx.push_back(elemstore.size()-1); + ++n; + + return t->h; + } + + void ReserveMore(int howMuch) { + elemstore.reserve(elemstore.size() + howMuch); + elemidx.reserve(elemidx.size() + howMuch); + // freelist.reserve(freelist.size() + howMuch); // PAR@@@@ maybe we should - not much more RAM + } + + void Add(T *t) { + // Look to see if we already have something with the same handle value. + ssassert(FindByIdNoOops(t->h) == nullptr, "Handle isn't unique"); + + // Find out where the added element should be. + auto pos = std::lower_bound(elemidx.begin(), elemidx.end(), *t, Compare(this)); + + if(freelist.empty()) { // Add a new element to the store + elemstore.push_back(*t); + // Insert a pointer to the element at the correct position + if(elemidx.empty()) { + // The list is empty so pos, begin and end are all null. + // insert does not work in this case. + elemidx.push_back(elemstore.size()-1); + } else { + elemidx.insert(pos, elemstore.size() - 1); + } + } else { // Use the last element from the freelist + // Insert an index to the element at the correct position + elemidx.insert(pos, freelist.back()); + // Remove the element from the freelist + freelist.pop_back(); + + // Copy-construct to the element storage. + elemstore[*pos] = T(*t); + // *elemptr[pos] = *t; // PAR@@@@@@ maybe this? + } + + ++n; + } + + T *FindById(H h) { + T *t = FindByIdNoOops(h); + ssassert(t != nullptr, "Cannot find handle"); + return t; + } + + T *FindByIdNoOops(H h) { + if(IsEmpty()) { + return nullptr; + } + auto it = std::lower_bound(elemidx.begin(), elemidx.end(), h, Compare(this)); + if(it == elemidx.end()) { + return nullptr; + } else { + if(elemstore[*it].h.v != h.v) { + return nullptr; + } + return &elemstore[*it]; + } + } + + T &Get(size_t i) { return elemstore[elemidx[i]]; } + T &operator[](size_t i) { return Get(i); } + + iterator begin() { return IsEmpty() ? nullptr : iterator(this); } + iterator end() { return IsEmpty() ? nullptr : iterator(this, elemidx.size()); } + + void ClearTags() { + for(auto &elt : *this) { elt.tag = 0; } + } + + void Tag(H h, int tag) { + auto it = FindByIdNoOops(h); + if (it != nullptr) { + it->tag = tag; + } + } + + void RemoveTagged() { + int src, dest; + dest = 0; + for(src = 0; src < n; src++) { + if(elemstore[elemidx[src]].tag) { + // this item should be deleted + elemstore[elemidx[src]].Clear(); +// elemstore[elemidx[src]].~T(); // Clear below calls the destructors + freelist.push_back(elemidx[src]); + elemidx[src] = 0xDEADBEEF; // PAR@@@@@ just for debugging, not needed, remove later + } else { + if(src != dest) { + elemidx[dest] = elemidx[src]; + } + dest++; + } + } + n = dest; + elemidx.resize(n); // Clear left over elements at the end. + } + void RemoveById(H h) { // PAR@@@@@ this can be optimized + ClearTags(); + FindById(h)->tag = 1; + RemoveTagged(); + } + + void MoveSelfInto(IdList *l) { + l->Clear(); + std::swap(l->elemstore, elemstore); + std::swap(l->elemidx, elemidx); + std::swap(l->freelist, freelist); + std::swap(l->n, n); + } + + void DeepCopyInto(IdList *l) { + l->Clear(); + + for(auto const &it : elemstore) { + l->elemstore.push_back(it); + } + + for(auto const &it : elemidx) { + l->elemidx.push_back(it); + } + + l->n = n; + } + + void Clear() { + for(auto &it : elemidx) { + elemstore[it].Clear(); +// elemstore[it].~T(); // clear below calls the destructors + } + freelist.clear(); + elemidx.clear(); + elemstore.clear(); + n = 0; + } + +}; + +class BandedMatrix { +public: + enum { + MAX_UNKNOWNS = 16, + RIGHT_OF_DIAG = 1, + LEFT_OF_DIAG = 2 + }; + + double A[MAX_UNKNOWNS][MAX_UNKNOWNS]; + double B[MAX_UNKNOWNS]; + double X[MAX_UNKNOWNS]; + int n; + + void Solve(); +}; + +#define RGBi(r, g, b) RgbaColor::From((r), (g), (b)) +#define RGBf(r, g, b) RgbaColor::FromFloat((float)(r), (float)(g), (float)(b)) + +// Note: sizeof(class RgbaColor) should be exactly 4 +// +class RgbaColor { +public: + uint8_t red, green, blue, alpha; + + float redF() const { return (float)red / 255.0f; } + float greenF() const { return (float)green / 255.0f; } + float blueF() const { return (float)blue / 255.0f; } + float alphaF() const { return (float)alpha / 255.0f; } + + bool IsEmpty() const { return alpha == 0; } + + bool Equals(RgbaColor c) const { + return + c.red == red && + c.green == green && + c.blue == blue && + c.alpha == alpha; + } + + RgbaColor WithAlpha(uint8_t newAlpha) const { + RgbaColor color = *this; + color.alpha = newAlpha; + return color; + } + + uint32_t ToPackedIntBGRA() const { + return + blue | + (uint32_t)(green << 8) | + (uint32_t)(red << 16) | + (uint32_t)((255 - alpha) << 24); + } + + uint32_t ToPackedInt() const { + return + red | + (uint32_t)(green << 8) | + (uint32_t)(blue << 16) | + (uint32_t)((255 - alpha) << 24); + } + + uint32_t ToARGB32() const { + return + blue | + (uint32_t)(green << 8) | + (uint32_t)(red << 16) | + (uint32_t)(alpha << 24); + } + + static RgbaColor From(int r, int g, int b, int a = 255) { + RgbaColor c; + c.red = (uint8_t)r; + c.green = (uint8_t)g; + c.blue = (uint8_t)b; + c.alpha = (uint8_t)a; + return c; + } + + static RgbaColor FromFloat(float r, float g, float b, float a = 1.0) { + return From( + (int)(255.1f * r), + (int)(255.1f * g), + (int)(255.1f * b), + (int)(255.1f * a)); + } + + static RgbaColor FromPackedInt(uint32_t rgba) { + return From( + (int)((rgba) & 0xff), + (int)((rgba >> 8) & 0xff), + (int)((rgba >> 16) & 0xff), + (int)(255 - ((rgba >> 24) & 0xff))); + } + + static RgbaColor FromPackedIntBGRA(uint32_t bgra) { + return From( + (int)((bgra >> 16) & 0xff), + (int)((bgra >> 8) & 0xff), + (int)((bgra) & 0xff), + (int)(255 - ((bgra >> 24) & 0xff))); + } +}; + +struct RgbaColorCompare { + bool operator()(RgbaColor a, RgbaColor b) const { + return a.ToARGB32() < b.ToARGB32(); + } +}; + +class BBox { +public: + Vector minp; + Vector maxp; + + static BBox From(const Vector &p0, const Vector &p1); + + Vector GetOrigin() const; + Vector GetExtents() const; + + void Include(const Vector &v, double r = 0.0); + bool Overlaps(const BBox &b1) const; + bool Contains(const Point2d &p, double r = 0.0) const; +}; + +} // namespace SolveSpace + +#endif diff --git a/src/entity.cpp b/src/entity.cpp new file mode 100644 index 0000000..7b7b7a0 --- /dev/null +++ b/src/entity.cpp @@ -0,0 +1,997 @@ +//----------------------------------------------------------------------------- +// The implementation of our entities in the symbolic algebra system, methods +// to return a symbolic representation of the entity (line by its endpoints, +// circle by center and radius, etc.). +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +const hEntity EntityBase::FREE_IN_3D = { 0 }; +const hEntity EntityBase::NO_ENTITY = { 0 }; + +bool EntityBase::HasVector() const { + switch(type) { + case Type::LINE_SEGMENT: + case Type::NORMAL_IN_3D: + case Type::NORMAL_IN_2D: + case Type::NORMAL_N_COPY: + case Type::NORMAL_N_ROT: + case Type::NORMAL_N_ROT_AA: + return true; + + default: + return false; + } +} + +ExprVector EntityBase::VectorGetExprsInWorkplane(hEntity wrkpl) const { + if(IsFace()) { + return FaceGetNormalExprs(); + } + switch(type) { + case Type::LINE_SEGMENT: + return (SK.GetEntity(point[0])->PointGetExprsInWorkplane(wrkpl)).Minus( + SK.GetEntity(point[1])->PointGetExprsInWorkplane(wrkpl)); + + case Type::NORMAL_IN_3D: + case Type::NORMAL_IN_2D: + case Type::NORMAL_N_COPY: + case Type::NORMAL_N_ROT: + case Type::NORMAL_N_ROT_AA: { + ExprVector ev = NormalExprsN(); + if(wrkpl == EntityBase::FREE_IN_3D) { + return ev; + } + // Get the offset and basis vectors for this weird exotic csys. + EntityBase *w = SK.GetEntity(wrkpl); + ExprVector wu = w->Normal()->NormalExprsU(); + ExprVector wv = w->Normal()->NormalExprsV(); + + // Get our coordinates in three-space, and project them into that + // coordinate system. + ExprVector result; + result.x = ev.Dot(wu); + result.y = ev.Dot(wv); + result.z = Expr::From(0.0); + return result; + } + default: ssassert(false, "Unexpected entity type"); + } +} + +ExprVector EntityBase::VectorGetExprs() const { + return VectorGetExprsInWorkplane(EntityBase::FREE_IN_3D); +} + +Vector EntityBase::VectorGetNum() const { + if(IsFace()) { + return FaceGetNormalNum(); + } + switch(type) { + case Type::LINE_SEGMENT: + return (SK.GetEntity(point[0])->PointGetNum()).Minus( + SK.GetEntity(point[1])->PointGetNum()); + + case Type::NORMAL_IN_3D: + case Type::NORMAL_IN_2D: + case Type::NORMAL_N_COPY: + case Type::NORMAL_N_ROT: + case Type::NORMAL_N_ROT_AA: + return NormalN(); + + default: ssassert(false, "Unexpected entity type"); + } +} + +Vector EntityBase::VectorGetRefPoint() const { + if(IsFace()) { + return FaceGetPointNum(); + } + switch(type) { + case Type::LINE_SEGMENT: + return ((SK.GetEntity(point[0])->PointGetNum()).Plus( + SK.GetEntity(point[1])->PointGetNum())).ScaledBy(0.5); + + case Type::NORMAL_IN_3D: + case Type::NORMAL_IN_2D: + case Type::NORMAL_N_COPY: + case Type::NORMAL_N_ROT: + case Type::NORMAL_N_ROT_AA: + return SK.GetEntity(point[0])->PointGetNum(); + + default: ssassert(false, "Unexpected entity type"); + } +} + +Vector EntityBase::VectorGetStartPoint() const { + switch(type) { + case Type::LINE_SEGMENT: + return SK.GetEntity(point[1])->PointGetNum(); + + case Type::NORMAL_IN_3D: + case Type::NORMAL_IN_2D: + case Type::NORMAL_N_COPY: + case Type::NORMAL_N_ROT: + case Type::NORMAL_N_ROT_AA: + return SK.GetEntity(point[0])->PointGetNum(); + + default: ssassert(false, "Unexpected entity type"); + } +} + +bool EntityBase::IsCircle() const { + return (type == Type::CIRCLE) || (type == Type::ARC_OF_CIRCLE); +} + +Expr *EntityBase::CircleGetRadiusExpr() const { + if(type == Type::CIRCLE) { + return SK.GetEntity(distance)->DistanceGetExpr(); + } else if(type == Type::ARC_OF_CIRCLE) { + return Constraint::Distance(workplane, point[0], point[1]); + } else ssassert(false, "Unexpected entity type"); +} + +double EntityBase::CircleGetRadiusNum() const { + if(type == Type::CIRCLE) { + return SK.GetEntity(distance)->DistanceGetNum(); + } else if(type == Type::ARC_OF_CIRCLE) { + Vector c = SK.GetEntity(point[0])->PointGetNum(); + Vector pa = SK.GetEntity(point[1])->PointGetNum(); + return (pa.Minus(c)).Magnitude(); + } else ssassert(false, "Unexpected entity type"); +} + +void EntityBase::ArcGetAngles(double *thetaa, double *thetab, double *dtheta) const { + ssassert(type == Type::ARC_OF_CIRCLE, "Unexpected entity type"); + + Quaternion q = Normal()->NormalGetNum(); + Vector u = q.RotationU(), v = q.RotationV(); + + Vector c = SK.GetEntity(point[0])->PointGetNum(); + Vector pa = SK.GetEntity(point[1])->PointGetNum(); + Vector pb = SK.GetEntity(point[2])->PointGetNum(); + + Point2d c2 = c.Project2d(u, v); + Point2d pa2 = (pa.Project2d(u, v)).Minus(c2); + Point2d pb2 = (pb.Project2d(u, v)).Minus(c2); + + *thetaa = atan2(pa2.y, pa2.x); + *thetab = atan2(pb2.y, pb2.x); + *dtheta = *thetab - *thetaa; + // If the endpoints are coincident, call it a full arc, not a zero arc; + // useful concept to have when splitting + while(*dtheta < 1e-6) *dtheta += 2*PI; + while(*dtheta > (2*PI)) *dtheta -= 2*PI; +} + +Vector EntityBase::CubicGetStartNum() const { + return SK.GetEntity(point[0])->PointGetNum(); +} +Vector EntityBase::CubicGetFinishNum() const { + return SK.GetEntity(point[3+extraPoints])->PointGetNum(); +} +ExprVector EntityBase::CubicGetStartTangentExprs() const { + ExprVector pon = SK.GetEntity(point[0])->PointGetExprs(), + poff = SK.GetEntity(point[1])->PointGetExprs(); + return (pon.Minus(poff)); +} +ExprVector EntityBase::CubicGetFinishTangentExprs() const { + ExprVector pon = SK.GetEntity(point[3+extraPoints])->PointGetExprs(), + poff = SK.GetEntity(point[2+extraPoints])->PointGetExprs(); + return (pon.Minus(poff)); +} +Vector EntityBase::CubicGetStartTangentNum() const { + Vector pon = SK.GetEntity(point[0])->PointGetNum(), + poff = SK.GetEntity(point[1])->PointGetNum(); + return (pon.Minus(poff)); +} +Vector EntityBase::CubicGetFinishTangentNum() const { + Vector pon = SK.GetEntity(point[3+extraPoints])->PointGetNum(), + poff = SK.GetEntity(point[2+extraPoints])->PointGetNum(); + return (pon.Minus(poff)); +} + +bool EntityBase::IsWorkplane() const { + return (type == Type::WORKPLANE); +} + +ExprVector EntityBase::WorkplaneGetOffsetExprs() const { + return SK.GetEntity(point[0])->PointGetExprs(); +} + +Vector EntityBase::WorkplaneGetOffset() const { + return SK.GetEntity(point[0])->PointGetNum(); +} + +void EntityBase::WorkplaneGetPlaneExprs(ExprVector *n, Expr **dn) const { + if(type == Type::WORKPLANE) { + *n = Normal()->NormalExprsN(); + + ExprVector p0 = SK.GetEntity(point[0])->PointGetExprs(); + // The plane is n dot (p - p0) = 0, or + // n dot p - n dot p0 = 0 + // so dn = n dot p0 + *dn = p0.Dot(*n); + } else ssassert(false, "Unexpected entity type"); +} + +bool EntityBase::IsDistance() const { + return (type == Type::DISTANCE) || + (type == Type::DISTANCE_N_COPY); +} +double EntityBase::DistanceGetNum() const { + if(type == Type::DISTANCE) { + return SK.GetParam(param[0])->val; + } else if(type == Type::DISTANCE_N_COPY) { + return numDistance; + } else ssassert(false, "Unexpected entity type"); +} +Expr *EntityBase::DistanceGetExpr() const { + if(type == Type::DISTANCE) { + return Expr::From(param[0]); + } else if(type == Type::DISTANCE_N_COPY) { + return Expr::From(numDistance); + } else ssassert(false, "Unexpected entity type"); +} +void EntityBase::DistanceForceTo(double v) { + if(type == Type::DISTANCE) { + (SK.GetParam(param[0]))->val = v; + } else if(type == Type::DISTANCE_N_COPY) { + // do nothing, it's locked + } else ssassert(false, "Unexpected entity type"); +} + +EntityBase *EntityBase::Normal() const { + return SK.GetEntity(normal); +} + +bool EntityBase::IsPoint() const { + switch(type) { + case Type::POINT_IN_3D: + case Type::POINT_IN_2D: + case Type::POINT_N_COPY: + case Type::POINT_N_TRANS: + case Type::POINT_N_ROT_TRANS: + case Type::POINT_N_ROT_AA: + case Type::POINT_N_ROT_AXIS_TRANS: + return true; + + default: + return false; + } +} + +bool EntityBase::IsNormal() const { + switch(type) { + case Type::NORMAL_IN_3D: + case Type::NORMAL_IN_2D: + case Type::NORMAL_N_COPY: + case Type::NORMAL_N_ROT: + case Type::NORMAL_N_ROT_AA: + return true; + + default: return false; + } +} + +Quaternion EntityBase::NormalGetNum() const { + Quaternion q; + switch(type) { + case Type::NORMAL_IN_3D: + q = Quaternion::From(param[0], param[1], param[2], param[3]); + break; + + case Type::NORMAL_IN_2D: { + EntityBase *wrkpl = SK.GetEntity(workplane); + EntityBase *norm = SK.GetEntity(wrkpl->normal); + q = norm->NormalGetNum(); + break; + } + case Type::NORMAL_N_COPY: + q = numNormal; + break; + + case Type::NORMAL_N_ROT: + q = Quaternion::From(param[0], param[1], param[2], param[3]); + q = q.Times(numNormal); + break; + + case Type::NORMAL_N_ROT_AA: { + q = GetAxisAngleQuaternion(0); + q = q.Times(numNormal); + break; + } + + default: ssassert(false, "Unexpected entity type"); + } + return q; +} + +void EntityBase::NormalForceTo(Quaternion q) { + switch(type) { + case Type::NORMAL_IN_3D: + SK.GetParam(param[0])->val = q.w; + SK.GetParam(param[1])->val = q.vx; + SK.GetParam(param[2])->val = q.vy; + SK.GetParam(param[3])->val = q.vz; + break; + + case Type::NORMAL_IN_2D: + case Type::NORMAL_N_COPY: + // There's absolutely nothing to do; these are locked. + break; + case Type::NORMAL_N_ROT: { + Quaternion qp = q.Times(numNormal.Inverse()); + + SK.GetParam(param[0])->val = qp.w; + SK.GetParam(param[1])->val = qp.vx; + SK.GetParam(param[2])->val = qp.vy; + SK.GetParam(param[3])->val = qp.vz; + break; + } + + case Type::NORMAL_N_ROT_AA: + // Not sure if I'll bother implementing this one + break; + + default: ssassert(false, "Unexpected entity type"); + } +} + +Vector EntityBase::NormalU() const { + return NormalGetNum().RotationU(); +} +Vector EntityBase::NormalV() const { + return NormalGetNum().RotationV(); +} +Vector EntityBase::NormalN() const { + return NormalGetNum().RotationN(); +} + +ExprVector EntityBase::NormalExprsU() const { + return NormalGetExprs().RotationU(); +} +ExprVector EntityBase::NormalExprsV() const { + return NormalGetExprs().RotationV(); +} +ExprVector EntityBase::NormalExprsN() const { + return NormalGetExprs().RotationN(); +} + +ExprQuaternion EntityBase::NormalGetExprs() const { + ExprQuaternion q; + switch(type) { + case Type::NORMAL_IN_3D: + q = ExprQuaternion::From(param[0], param[1], param[2], param[3]); + break; + + case Type::NORMAL_IN_2D: { + EntityBase *wrkpl = SK.GetEntity(workplane); + EntityBase *norm = SK.GetEntity(wrkpl->normal); + q = norm->NormalGetExprs(); + break; + } + case Type::NORMAL_N_COPY: + q = ExprQuaternion::From(numNormal); + break; + + case Type::NORMAL_N_ROT: { + ExprQuaternion orig = ExprQuaternion::From(numNormal); + q = ExprQuaternion::From(param[0], param[1], param[2], param[3]); + + q = q.Times(orig); + break; + } + + case Type::NORMAL_N_ROT_AA: { + ExprQuaternion orig = ExprQuaternion::From(numNormal); + q = GetAxisAngleQuaternionExprs(0); + q = q.Times(orig); + break; + } + + default: ssassert(false, "Unexpected entity type"); + } + return q; +} + +void EntityBase::PointForceParamTo(Vector p) { + switch(type) { + case Type::POINT_IN_3D: + SK.GetParam(param[0])->val = p.x; + SK.GetParam(param[1])->val = p.y; + SK.GetParam(param[2])->val = p.z; + break; + + case Type::POINT_IN_2D: + SK.GetParam(param[0])->val = p.x; + SK.GetParam(param[1])->val = p.y; + break; + + default: ssassert(false, "Unexpected entity type"); + } +} + +void EntityBase::PointForceTo(Vector p) { + switch(type) { + case Type::POINT_IN_3D: + SK.GetParam(param[0])->val = p.x; + SK.GetParam(param[1])->val = p.y; + SK.GetParam(param[2])->val = p.z; + break; + + case Type::POINT_IN_2D: { + EntityBase *c = SK.GetEntity(workplane); + p = p.Minus(c->WorkplaneGetOffset()); + SK.GetParam(param[0])->val = p.Dot(c->Normal()->NormalU()); + SK.GetParam(param[1])->val = p.Dot(c->Normal()->NormalV()); + break; + } + + case Type::POINT_N_TRANS: { + if(timesApplied == 0) break; + Vector trans = (p.Minus(numPoint)).ScaledBy(1.0/timesApplied); + SK.GetParam(param[0])->val = trans.x; + SK.GetParam(param[1])->val = trans.y; + SK.GetParam(param[2])->val = trans.z; + break; + } + + case Type::POINT_N_ROT_TRANS: { + // Force only the translation; leave the rotation unchanged. But + // remember that we're working with respect to the rotated + // point. + Vector trans = p.Minus(PointGetQuaternion().Rotate(numPoint)); + SK.GetParam(param[0])->val = trans.x; + SK.GetParam(param[1])->val = trans.y; + SK.GetParam(param[2])->val = trans.z; + break; + } + + case Type::POINT_N_ROT_AA: { + // Force only the angle; the axis and center of rotation stay + Vector offset = Vector::From(param[0], param[1], param[2]); + Vector normal = Vector::From(param[4], param[5], param[6]); + Vector u = normal.Normal(0), v = normal.Normal(1); + Vector po = p.Minus(offset), numo = numPoint.Minus(offset); + double thetap = atan2(v.Dot(po), u.Dot(po)); + double thetan = atan2(v.Dot(numo), u.Dot(numo)); + double thetaf = (thetap - thetan); + double thetai = (SK.GetParam(param[3])->val)*timesApplied*2; + double dtheta = thetaf - thetai; + // Take the smallest possible change in the actual step angle, + // in order to avoid jumps when you cross from +pi to -pi + while(dtheta < -PI) dtheta += 2*PI; + while(dtheta > PI) dtheta -= 2*PI; + // this extra *2 explains the mystery *4 + SK.GetParam(param[3])->val = (thetai + dtheta)/(timesApplied*2); + break; + } + + case Type::POINT_N_ROT_AXIS_TRANS: { + if(timesApplied == 0) break; + // is the point on the rotation axis? + Vector offset = Vector::From(param[0], param[1], param[2]); + Vector normal = Vector::From(param[4], param[5], param[6]).WithMagnitude(1.0); + Vector check = numPoint.Minus(offset).Cross(normal); + if (check.Dot(check) < LENGTH_EPS) { // if so, do extrusion style drag + Vector trans = (p.Minus(numPoint)); + SK.GetParam(param[7])->val = trans.Dot(normal)/timesApplied; + } else { // otherwise do rotation style + Vector u = normal.Normal(0), v = normal.Normal(1); + Vector po = p.Minus(offset), numo = numPoint.Minus(offset); + double thetap = atan2(v.Dot(po), u.Dot(po)); + double thetan = atan2(v.Dot(numo), u.Dot(numo)); + double thetaf = (thetap - thetan); + double thetai = (SK.GetParam(param[3])->val)*timesApplied*2; + double dtheta = thetaf - thetai; + // Take the smallest possible change in the actual step angle, + // in order to avoid jumps when you cross from +pi to -pi + while(dtheta < -PI) dtheta += 2*PI; + while(dtheta > PI) dtheta -= 2*PI; + // this extra *2 explains the mystery *4 + SK.GetParam(param[3])->val = (thetai + dtheta)/(timesApplied*2); + } + break; + } + + case Type::POINT_N_COPY: + // Nothing to do; it's a static copy + break; + + default: ssassert(false, "Unexpected entity type"); + } +} + +Vector EntityBase::PointGetNum() const { + Vector p; + switch(type) { + case Type::POINT_IN_3D: + p = Vector::From(param[0], param[1], param[2]); + break; + + case Type::POINT_IN_2D: { + EntityBase *c = SK.GetEntity(workplane); + Vector u = c->Normal()->NormalU(); + Vector v = c->Normal()->NormalV(); + p = u.ScaledBy(SK.GetParam(param[0])->val); + p = p.Plus(v.ScaledBy(SK.GetParam(param[1])->val)); + p = p.Plus(c->WorkplaneGetOffset()); + break; + } + + case Type::POINT_N_TRANS: { + Vector trans = Vector::From(param[0], param[1], param[2]); + p = numPoint.Plus(trans.ScaledBy(timesApplied)); + break; + } + + case Type::POINT_N_ROT_TRANS: { + Vector offset = Vector::From(param[0], param[1], param[2]); + Quaternion q = PointGetQuaternion(); + p = q.Rotate(numPoint); + p = p.Plus(offset); + break; + } + + case Type::POINT_N_ROT_AA: { + Vector offset = Vector::From(param[0], param[1], param[2]); + Quaternion q = PointGetQuaternion(); + p = numPoint.Minus(offset); + p = q.Rotate(p); + p = p.Plus(offset); + break; + } + + case Type::POINT_N_ROT_AXIS_TRANS: { + Vector offset = Vector::From(param[0], param[1], param[2]); + Vector displace = Vector::From(param[4], param[5], param[6]) + .WithMagnitude(SK.GetParam(param[7])->val).ScaledBy(timesApplied); + Quaternion q = PointGetQuaternion(); + p = numPoint.Minus(offset); + p = q.Rotate(p); + p = p.Plus(offset).Plus(displace); + break; + } + + case Type::POINT_N_COPY: + p = numPoint; + break; + + default: ssassert(false, "Unexpected entity type"); + } + return p; +} + +ExprVector EntityBase::PointGetExprs() const { + ExprVector r; + switch(type) { + case Type::POINT_IN_3D: + r = ExprVector::From(param[0], param[1], param[2]); + break; + + case Type::POINT_IN_2D: { + EntityBase *c = SK.GetEntity(workplane); + ExprVector u = c->Normal()->NormalExprsU(); + ExprVector v = c->Normal()->NormalExprsV(); + r = c->WorkplaneGetOffsetExprs(); + r = r.Plus(u.ScaledBy(Expr::From(param[0]))); + r = r.Plus(v.ScaledBy(Expr::From(param[1]))); + break; + } + case Type::POINT_N_TRANS: { + ExprVector orig = ExprVector::From(numPoint); + ExprVector trans = ExprVector::From(param[0], param[1], param[2]); + r = orig.Plus(trans.ScaledBy(Expr::From(timesApplied))); + break; + } + case Type::POINT_N_ROT_TRANS: { + ExprVector orig = ExprVector::From(numPoint); + ExprVector trans = ExprVector::From(param[0], param[1], param[2]); + ExprQuaternion q = + ExprQuaternion::From(param[3], param[4], param[5], param[6]); + orig = q.Rotate(orig); + r = orig.Plus(trans); + break; + } + case Type::POINT_N_ROT_AA: { + ExprVector orig = ExprVector::From(numPoint); + ExprVector trans = ExprVector::From(param[0], param[1], param[2]); + ExprQuaternion q = GetAxisAngleQuaternionExprs(3); + orig = orig.Minus(trans); + orig = q.Rotate(orig); + r = orig.Plus(trans); + break; + } + case Type::POINT_N_ROT_AXIS_TRANS: { + ExprVector orig = ExprVector::From(numPoint); + ExprVector trans = ExprVector::From(param[0], param[1], param[2]); + ExprVector displace = ExprVector::From(param[4], param[5], param[6]) + .WithMagnitude(Expr::From(1.0)).ScaledBy(Expr::From(timesApplied)).ScaledBy(Expr::From(param[7])); + + ExprQuaternion q = GetAxisAngleQuaternionExprs(3); + orig = orig.Minus(trans); + orig = q.Rotate(orig); + r = orig.Plus(trans).Plus(displace); + break; + } + case Type::POINT_N_COPY: + r = ExprVector::From(numPoint); + break; + + default: ssassert(false, "Unexpected entity type"); + } + return r; +} + +void EntityBase::PointGetExprsInWorkplane(hEntity wrkpl, Expr **u, Expr **v) const { + if(type == Type::POINT_IN_2D && workplane == wrkpl) { + // They want our coordinates in the form that we've written them, + // very nice. + *u = Expr::From(param[0]); + *v = Expr::From(param[1]); + } else { + // Get the offset and basis vectors for this weird exotic csys. + EntityBase *w = SK.GetEntity(wrkpl); + ExprVector wp = w->WorkplaneGetOffsetExprs(); + ExprVector wu = w->Normal()->NormalExprsU(); + ExprVector wv = w->Normal()->NormalExprsV(); + + // Get our coordinates in three-space, and project them into that + // coordinate system. + ExprVector ev = PointGetExprs(); + ev = ev.Minus(wp); + *u = ev.Dot(wu); + *v = ev.Dot(wv); + } +} + +ExprVector EntityBase::PointGetExprsInWorkplane(hEntity wrkpl) const { + if(wrkpl == Entity::FREE_IN_3D) { + return PointGetExprs(); + } + + ExprVector r; + PointGetExprsInWorkplane(wrkpl, &r.x, &r.y); + r.z = Expr::From(0.0); + return r; +} + +void EntityBase::PointForceQuaternionTo(Quaternion q) { + ssassert(type == Type::POINT_N_ROT_TRANS, "Unexpected entity type"); + + SK.GetParam(param[3])->val = q.w; + SK.GetParam(param[4])->val = q.vx; + SK.GetParam(param[5])->val = q.vy; + SK.GetParam(param[6])->val = q.vz; +} + +Quaternion EntityBase::GetAxisAngleQuaternion(int param0) const { + Quaternion q; + double theta = timesApplied*SK.GetParam(param[param0+0])->val; + double s = sin(theta), c = cos(theta); + q.w = c; + q.vx = s*SK.GetParam(param[param0+1])->val; + q.vy = s*SK.GetParam(param[param0+2])->val; + q.vz = s*SK.GetParam(param[param0+3])->val; + return q; +} + +ExprQuaternion EntityBase::GetAxisAngleQuaternionExprs(int param0) const { + ExprQuaternion q; + + Expr *theta = Expr::From(timesApplied)->Times( + Expr::From(param[param0+0])); + Expr *c = theta->Cos(), *s = theta->Sin(); + q.w = c; + q.vx = s->Times(Expr::From(param[param0+1])); + q.vy = s->Times(Expr::From(param[param0+2])); + q.vz = s->Times(Expr::From(param[param0+3])); + return q; +} + +Quaternion EntityBase::PointGetQuaternion() const { + Quaternion q; + + if(type == Type::POINT_N_ROT_AA || type == Type::POINT_N_ROT_AXIS_TRANS) { + q = GetAxisAngleQuaternion(3); + } else if(type == Type::POINT_N_ROT_TRANS) { + q = Quaternion::From(param[3], param[4], param[5], param[6]); + } else ssassert(false, "Unexpected entity type"); + + return q; +} + +bool EntityBase::IsFace() const { + switch(type) { + case Type::FACE_NORMAL_PT: + case Type::FACE_XPROD: + case Type::FACE_N_ROT_TRANS: + case Type::FACE_N_TRANS: + case Type::FACE_N_ROT_AA: + case Type::FACE_ROT_NORMAL_PT: + case Type::FACE_N_ROT_AXIS_TRANS: + return true; + default: + return false; + } +} + +ExprVector EntityBase::FaceGetNormalExprs() const { + ExprVector r; + if(type == Type::FACE_NORMAL_PT) { + Vector v = Vector::From(numNormal.vx, numNormal.vy, numNormal.vz); + r = ExprVector::From(v.WithMagnitude(1)); + } else if(type == Type::FACE_XPROD) { + ExprVector vc = ExprVector::From(param[0], param[1], param[2]); + ExprVector vn = + ExprVector::From(numNormal.vx, numNormal.vy, numNormal.vz); + r = vc.Cross(vn); + r = r.WithMagnitude(Expr::From(1.0)); + } else if(type == Type::FACE_N_ROT_TRANS) { + // The numerical normal vector gets the rotation; the numerical + // normal has magnitude one, and the rotation doesn't change that, + // so there's no need to fix it up. + r = ExprVector::From(numNormal.vx, numNormal.vy, numNormal.vz); + ExprQuaternion q = + ExprQuaternion::From(param[3], param[4], param[5], param[6]); + r = q.Rotate(r); + } else if(type == Type::FACE_N_TRANS) { + r = ExprVector::From(numNormal.vx, numNormal.vy, numNormal.vz); + } else if((type == Type::FACE_N_ROT_AA) || (type == Type::FACE_ROT_NORMAL_PT)) { + r = ExprVector::From(numNormal.vx, numNormal.vy, numNormal.vz); + ExprQuaternion q = GetAxisAngleQuaternionExprs(3); + r = q.Rotate(r); + } else ssassert(false, "Unexpected entity type"); + return r; +} + +Vector EntityBase::FaceGetNormalNum() const { + Vector r; + if(type == Type::FACE_NORMAL_PT) { + r = Vector::From(numNormal.vx, numNormal.vy, numNormal.vz); + } else if(type == Type::FACE_XPROD) { + Vector vc = Vector::From(param[0], param[1], param[2]); + Vector vn = Vector::From(numNormal.vx, numNormal.vy, numNormal.vz); + r = vc.Cross(vn); + } else if(type == Type::FACE_N_ROT_TRANS) { + // The numerical normal vector gets the rotation + r = Vector::From(numNormal.vx, numNormal.vy, numNormal.vz); + Quaternion q = Quaternion::From(param[3], param[4], param[5], param[6]); + r = q.Rotate(r); + } else if(type == Type::FACE_N_TRANS) { + r = Vector::From(numNormal.vx, numNormal.vy, numNormal.vz); + } else if((type == Type::FACE_N_ROT_AA) || (type == Type::FACE_ROT_NORMAL_PT)) { + r = Vector::From(numNormal.vx, numNormal.vy, numNormal.vz); + Quaternion q = GetAxisAngleQuaternion(3); + r = q.Rotate(r); + } else ssassert(false, "Unexpected entity type"); + return r.WithMagnitude(1); +} + +ExprVector EntityBase::FaceGetPointExprs() const { + ExprVector r; + if((type == Type::FACE_NORMAL_PT) || (type==Type::FACE_ROT_NORMAL_PT)) { + r = SK.GetEntity(point[0])->PointGetExprs(); + } else if(type == Type::FACE_XPROD) { + r = ExprVector::From(numPoint); + } else if(type == Type::FACE_N_ROT_TRANS) { + // The numerical point gets the rotation and translation. + ExprVector trans = ExprVector::From(param[0], param[1], param[2]); + ExprQuaternion q = + ExprQuaternion::From(param[3], param[4], param[5], param[6]); + r = ExprVector::From(numPoint); + r = q.Rotate(r); + r = r.Plus(trans); + } else if(type == Type::FACE_N_ROT_AXIS_TRANS) { + ExprVector orig = ExprVector::From(numPoint); + ExprVector trans = ExprVector::From(param[0], param[1], param[2]); + ExprVector displace = ExprVector::From(param[4], param[5], param[6]) + .WithMagnitude(Expr::From(param[7])).ScaledBy(Expr::From(timesApplied)); + ExprQuaternion q = GetAxisAngleQuaternionExprs(3); + orig = orig.Minus(trans); + orig = q.Rotate(orig); + r = orig.Plus(trans).Plus(displace); + } else if(type == Type::FACE_N_TRANS) { + ExprVector trans = ExprVector::From(param[0], param[1], param[2]); + r = ExprVector::From(numPoint); + r = r.Plus(trans.ScaledBy(Expr::From(timesApplied))); + } else if(type == Type::FACE_N_ROT_AA) { + ExprVector trans = ExprVector::From(param[0], param[1], param[2]); + ExprQuaternion q = GetAxisAngleQuaternionExprs(3); + r = ExprVector::From(numPoint); + r = r.Minus(trans); + r = q.Rotate(r); + r = r.Plus(trans); + } else ssassert(false, "Unexpected entity type"); + return r; +} + +Vector EntityBase::FaceGetPointNum() const { + Vector r; + if((type == Type::FACE_NORMAL_PT) || (type==Type::FACE_ROT_NORMAL_PT)) { + r = SK.GetEntity(point[0])->PointGetNum(); + } else if(type == Type::FACE_XPROD) { + r = numPoint; + } else if(type == Type::FACE_N_ROT_TRANS) { + // The numerical point gets the rotation and translation. + Vector trans = Vector::From(param[0], param[1], param[2]); + Quaternion q = Quaternion::From(param[3], param[4], param[5], param[6]); + r = q.Rotate(numPoint); + r = r.Plus(trans); + } else if(type == Type::FACE_N_ROT_AXIS_TRANS) { + Vector offset = Vector::From(param[0], param[1], param[2]); + Vector displace = Vector::From(param[4], param[5], param[6]) + .WithMagnitude(SK.GetParam(param[7])->val).ScaledBy(timesApplied); + Quaternion q = PointGetQuaternion(); + r = numPoint.Minus(offset); + r = q.Rotate(r); + r = r.Plus(offset).Plus(displace); + } else if(type == Type::FACE_N_TRANS) { + Vector trans = Vector::From(param[0], param[1], param[2]); + r = numPoint.Plus(trans.ScaledBy(timesApplied)); + } else if(type == Type::FACE_N_ROT_AA) { + Vector trans = Vector::From(param[0], param[1], param[2]); + Quaternion q = GetAxisAngleQuaternion(3); + r = numPoint.Minus(trans); + r = q.Rotate(r); + r = r.Plus(trans); + } else ssassert(false, "Unexpected entity type"); + return r; +} + +bool EntityBase::HasEndpoints() const { + return (type == Type::LINE_SEGMENT) || + (type == Type::CUBIC) || + (type == Type::ARC_OF_CIRCLE); +} +Vector EntityBase::EndpointStart() const { + if(type == Type::LINE_SEGMENT) { + return SK.GetEntity(point[0])->PointGetNum(); + } else if(type == Type::CUBIC) { + return CubicGetStartNum(); + } else if(type == Type::ARC_OF_CIRCLE) { + return SK.GetEntity(point[1])->PointGetNum(); + } else ssassert(false, "Unexpected entity type"); +} +Vector EntityBase::EndpointFinish() const { + if(type == Type::LINE_SEGMENT) { + return SK.GetEntity(point[1])->PointGetNum(); + } else if(type == Type::CUBIC) { + return CubicGetFinishNum(); + } else if(type == Type::ARC_OF_CIRCLE) { + return SK.GetEntity(point[2])->PointGetNum(); + } else ssassert(false, "Unexpected entity type"); +} +static bool PointInPlane(hEntity h, Vector norm, double distance) { + Vector p = SK.GetEntity(h)->PointGetNum(); + return (fabs(norm.Dot(p) - distance) < LENGTH_EPS); +} +bool EntityBase::IsInPlane(Vector norm, double distance) const { + switch(type) { + case Type::LINE_SEGMENT: { + return PointInPlane(point[0], norm, distance) + && PointInPlane(point[1], norm, distance); + } + case Type::CUBIC: + case Type::CUBIC_PERIODIC: { + bool periodic = type == Type::CUBIC_PERIODIC; + int n = periodic ? 3 + extraPoints : extraPoints; + int i; + for (i=0; iNormalN(); + if (!norm.Equals(n) && !norm.Equals(n.Negated())) return false; + return PointInPlane(point[0], norm, distance); + } + + case Type::TTF_TEXT: { + Vector n = Normal()->NormalN(); + if (!norm.Equals(n) && !norm.Equals(n.Negated())) return false; + return PointInPlane(point[0], norm, distance) + && PointInPlane(point[1], norm, distance); + } + + default: + return false; + } +} + +void EntityBase::RectGetPointsExprs(ExprVector *eb, ExprVector *ec) const { + ssassert(type == Type::TTF_TEXT || type == Type::IMAGE, + "Unexpected entity type"); + + EntityBase *a = SK.GetEntity(point[0]); + EntityBase *o = SK.GetEntity(point[1]); + + // Write equations for each point in the current workplane. + // This reduces the complexity of resulting equations. + ExprVector ea = a->PointGetExprsInWorkplane(workplane); + ExprVector eo = o->PointGetExprsInWorkplane(workplane); + + // Take perpendicular vector and scale it by aspect ratio. + ExprVector eu = ea.Minus(eo); + ExprVector ev = ExprVector::From(eu.y, eu.x->Negate(), eu.z).ScaledBy(Expr::From(aspectRatio)); + + *eb = eo.Plus(ev); + *ec = eo.Plus(eu).Plus(ev); +} + +void EntityBase::AddEq(IdList *l, Expr *expr, int index) const { + Equation eq; + eq.e = expr; + eq.h = h.equation(index); + l->Add(&eq); +} + +void EntityBase::GenerateEquations(IdList *l) const { + switch(type) { + case Type::NORMAL_IN_3D: { + ExprQuaternion q = NormalGetExprs(); + AddEq(l, (q.Magnitude())->Minus(Expr::From(1)), 0); + break; + } + + case Type::ARC_OF_CIRCLE: { + // If this is a copied entity, with its point already fixed + // with respect to each other, then we don't want to generate + // the distance constraint! + if(SK.GetEntity(point[0])->type != Type::POINT_IN_2D) break; + + // If the two endpoints of the arc are constrained coincident + // (to make a complete circle), then our distance constraint + // would be redundant and therefore overconstrain things. + auto it = std::find_if(SK.constraint.begin(), SK.constraint.end(), + [&](ConstraintBase const &con) { + return (con.group == group) && + (con.type == Constraint::Type::POINTS_COINCIDENT) && + ((con.ptA == point[1] && con.ptB == point[2]) || + (con.ptA == point[2] && con.ptB == point[1])); + }); + if(it != SK.constraint.end()) { + break; + } + + Expr *ra = Constraint::Distance(workplane, point[0], point[1]); + Expr *rb = Constraint::Distance(workplane, point[0], point[2]); + AddEq(l, ra->Minus(rb), 0); + break; + } + + case Type::IMAGE: + case Type::TTF_TEXT: { + if(SK.GetEntity(point[0])->type != Type::POINT_IN_2D) break; + EntityBase *b = SK.GetEntity(point[2]); + EntityBase *c = SK.GetEntity(point[3]); + ExprVector eb = b->PointGetExprsInWorkplane(workplane); + ExprVector ec = c->PointGetExprsInWorkplane(workplane); + + ExprVector ebp, ecp; + RectGetPointsExprs(&ebp, &ecp); + + ExprVector beq = eb.Minus(ebp); + AddEq(l, beq.x, 0); + AddEq(l, beq.y, 1); + ExprVector ceq = ec.Minus(ecp); + AddEq(l, ceq.x, 2); + AddEq(l, ceq.y, 3); + break; + } + + default: // Most entities do not generate equations. + break; + } +} + +} // namespace SolveSpace diff --git a/src/export.cpp b/src/export.cpp new file mode 100644 index 0000000..5e6ad07 --- /dev/null +++ b/src/export.cpp @@ -0,0 +1,1272 @@ +//----------------------------------------------------------------------------- +// The 2d vector output stuff that isn't specific to any particular file +// format: getting the appropriate lines and curves, performing hidden line +// removal, calculating bounding boxes, and so on. Also raster and triangle +// mesh output. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" +#include "config.h" + +namespace SolveSpace { + +void SolveSpaceUI::ExportSectionTo(const Platform::Path &filename) { + Vector gn = (SS.GW.projRight).Cross(SS.GW.projUp); + gn = gn.WithMagnitude(1); + + Group *g = SK.GetGroup(SS.GW.activeGroup); + g->GenerateDisplayItems(); + if(g->displayMesh.IsEmpty()) { + Error(_("No solid model present; draw one with extrudes and revolves, " + "or use Export 2d View to export bare lines and curves.")); + return; + } + + // The plane in which the exported section lies; need this because we'll + // reorient from that plane into the xy plane before exporting. + Vector origin, u, v, n; + double d; + + SS.GW.GroupSelection(); + auto const &gs = SS.GW.gs; + if((gs.n == 0 && g->activeWorkplane != Entity::FREE_IN_3D)) { + Entity *wrkpl = SK.GetEntity(g->activeWorkplane); + origin = wrkpl->WorkplaneGetOffset(); + n = wrkpl->Normal()->NormalN(); + u = wrkpl->Normal()->NormalU(); + v = wrkpl->Normal()->NormalV(); + } else if(gs.n == 1 && gs.faces == 1) { + Entity *face = SK.GetEntity(gs.entity[0]); + origin = face->FaceGetPointNum(); + n = face->FaceGetNormalNum(); + if(n.Dot(gn) < 0) n = n.ScaledBy(-1); + u = n.Normal(0); + v = n.Normal(1); + } else if(gs.n == 3 && gs.vectors == 2 && gs.points == 1) { + Vector ut = SK.GetEntity(gs.entity[0])->VectorGetNum(), + vt = SK.GetEntity(gs.entity[1])->VectorGetNum(); + ut = ut.WithMagnitude(1); + vt = vt.WithMagnitude(1); + + if(fabs(SS.GW.projUp.Dot(vt)) < fabs(SS.GW.projUp.Dot(ut))) { + swap(ut, vt); + } + if(SS.GW.projRight.Dot(ut) < 0) ut = ut.ScaledBy(-1); + if(SS.GW.projUp. Dot(vt) < 0) vt = vt.ScaledBy(-1); + + origin = SK.GetEntity(gs.point[0])->PointGetNum(); + n = ut.Cross(vt); + u = ut.WithMagnitude(1); + v = (n.Cross(u)).WithMagnitude(1); + } else { + Error(_("Bad selection for export section. Please select:\n\n" + " * nothing, with an active workplane " + "(workplane is section plane)\n" + " * a face (section plane through face)\n" + " * a point and two line segments " + "(plane through point and parallel to lines)\n")); + return; + } + SS.GW.ClearSelection(); + + n = n.WithMagnitude(1); + d = origin.Dot(n); + + SEdgeList el = {}; + SBezierList bl = {}; + + // If there's a mesh, then grab the edges from it. + g->runningMesh.MakeEdgesInPlaneInto(&el, n, d); + + // If there's a shell, then grab the edges and possibly Beziers. + bool export_as_pwl = SS.exportPwlCurves || fabs(SS.exportOffset) > LENGTH_EPS; + g->runningShell.MakeSectionEdgesInto(n, d, &el, export_as_pwl ? NULL : &bl); + + // All of these are solid model edges, so use the appropriate style. + SEdge *se; + for(se = el.l.First(); se; se = el.l.NextAfter(se)) { + se->auxA = Style::SOLID_EDGE; + } + SBezier *sb; + for(sb = bl.l.First(); sb; sb = bl.l.NextAfter(sb)) { + sb->auxA = Style::SOLID_EDGE; + } + + // Remove all overlapping edges/beziers to merge the areas they describe. + el.CullExtraneousEdges(/*both=*/true); + bl.CullIdenticalBeziers(/*both=*/true); + + // Collect lines and beziers with custom style & export. + for(auto &ent : SK.entity) { + Entity *e = &ent; + if (!e->IsVisible()) continue; + if (e->style.v < Style::FIRST_CUSTOM) continue; + if (!Style::Exportable(e->style.v)) continue; + if (!e->IsInPlane(n,d)) continue; + if (export_as_pwl) { + e->GenerateEdges(&el); + } else { + e->GenerateBezierCurves(&bl); + } + } + + // Only remove half of the overlapping edges/beziers to support TTF Stick Fonts. + el.CullExtraneousEdges(/*both=*/false); + bl.CullIdenticalBeziers(/*both=*/false); + + // And write the edges. + VectorFileWriter *out = VectorFileWriter::ForFile(filename); + if(out) { + // parallel projection (no perspective), and no mesh + ExportLinesAndMesh(&el, &bl, NULL, + u, v, n, origin, 0, + out); + } + el.Clear(); + bl.Clear(); +} + +// This is an awful temporary hack to replace Constraint::GetEdges until we have proper +// export through Canvas. +class GetEdgesCanvas : public Canvas { +public: + Camera camera; + SEdgeList *edges; + + const Camera &GetCamera() const override { + return camera; + } + + void DrawLine(const Vector &a, const Vector &b, hStroke hcs) override { + edges->AddEdge(a, b, Style::CONSTRAINT); + } + void DrawEdges(const SEdgeList &el, hStroke hcs) override { + for(const SEdge &e : el.l) { + edges->AddEdge(e.a, e.b, Style::CONSTRAINT); + } + } + void DrawVectorText(const std::string &text, double height, + const Vector &o, const Vector &u, const Vector &v, + hStroke hcs) override { + auto traceEdge = [&](Vector a, Vector b) { edges->AddEdge(a, b, Style::CONSTRAINT); }; + VectorFont::Builtin()->Trace(height, o, u, v, text, traceEdge, camera); + } + + void DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + hFill hcf) override { + // Do nothing + } + + bool DrawBeziers(const SBezierList &bl, hStroke hcs) override { + ssassert(false, "Not implemented"); + } + void DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs drawAs) override { + ssassert(false, "Not implemented"); + } + void DrawPoint(const Vector &o, hStroke hcs) override { + ssassert(false, "Not implemented"); + } + void DrawPolygon(const SPolygon &p, hFill hcf) override { + ssassert(false, "Not implemented"); + } + void DrawMesh(const SMesh &m, hFill hcfFront, hFill hcfBack = {}) override { + ssassert(false, "Not implemented"); + } + void DrawFaces(const SMesh &m, const std::vector &faces, hFill hcf) override { + ssassert(false, "Not implemented"); + } + void DrawPixmap(std::shared_ptr pm, + const Vector &o, const Vector &u, const Vector &v, + const Point2d &ta, const Point2d &tb, hFill hcf) override { + ssassert(false, "Not implemented"); + } + void InvalidatePixmap(std::shared_ptr pm) override { + ssassert(false, "Not implemented"); + } +}; + +void SolveSpaceUI::ExportViewOrWireframeTo(const Platform::Path &filename, bool exportWireframe) { + SEdgeList edges = {}; + SBezierList beziers = {}; + + VectorFileWriter *out = VectorFileWriter::ForFile(filename); + if(!out) return; + + SS.exportMode = true; + GenerateAll(Generate::ALL); + + SMesh *sm = NULL; + if(SS.GW.showShaded || SS.GW.drawOccludedAs != GraphicsWindow::DrawOccludedAs::VISIBLE) { + Group *g = SK.GetGroup(SS.GW.activeGroup); + g->GenerateDisplayItems(); + sm = &(g->displayMesh); + } + if(sm && sm->IsEmpty()) { + sm = NULL; + } + + for(auto &entity : SK.entity) { + Entity *e = &entity; + if(!e->IsVisible()) continue; + + if(SS.exportPwlCurves || sm || fabs(SS.exportOffset) > LENGTH_EPS) + { + // We will be doing hidden line removal, which we can't do on + // exact curves; so we need things broken down to pwls. Same + // problem with cutter radius compensation. + e->GenerateEdges(&edges); + } else { + e->GenerateBezierCurves(&beziers); + } + } + + if(SS.GW.showEdges || SS.GW.showOutlines) { + Group *g = SK.GetGroup(SS.GW.activeGroup); + g->GenerateDisplayItems(); + if(SS.GW.showEdges) { + g->displayOutlines.ListTaggedInto(&edges, Style::SOLID_EDGE); + } + } + + if(SS.GW.showConstraints != GraphicsWindow::ShowConstraintMode::SCM_NOSHOW ) { + if(!out->OutputConstraints(&SK.constraint)) { + GetEdgesCanvas canvas = {}; + canvas.camera = SS.GW.GetCamera(); + canvas.edges = &edges; + + // The output format cannot represent constraints directly, + // so convert them to edges. + for(Constraint &c : SK.constraint) { + c.Draw(Constraint::DrawAs::DEFAULT, &canvas); + } + + canvas.Clear(); + } + } + + if(exportWireframe) { + Vector u = Vector::From(1.0, 0.0, 0.0), + v = Vector::From(0.0, 1.0, 0.0), + n = Vector::From(0.0, 0.0, 1.0), + origin = Vector::From(0.0, 0.0, 0.0); + double cameraTan = 0.0, + scale = 1.0; + + out->SetModelviewProjection(u, v, n, origin, cameraTan, scale); + + ExportWireframeCurves(&edges, &beziers, out); + } else { + Vector u = SS.GW.projRight, + v = SS.GW.projUp, + n = u.Cross(v), + origin = SS.GW.offset.ScaledBy(-1); + + out->SetModelviewProjection(u, v, n, origin, + SS.CameraTangent()*SS.GW.scale, SS.exportScale); + + ExportLinesAndMesh(&edges, &beziers, sm, + u, v, n, origin, SS.CameraTangent()*SS.GW.scale, + out); + + if(!out->HasCanvasSize()) { + // These file formats don't have a canvas size, so they just + // get exported in the raw coordinate system. So indicate what + // that was on-screen. + SS.justExportedInfo.showOrigin = true; + SS.justExportedInfo.pt = origin; + SS.justExportedInfo.u = u; + SS.justExportedInfo.v = v; + } else { + SS.justExportedInfo.showOrigin = false; + } + + SS.justExportedInfo.draw = true; + GW.Invalidate(); + } + + edges.Clear(); + beziers.Clear(); +} + +void SolveSpaceUI::ExportWireframeCurves(SEdgeList *sel, SBezierList *sbl, + VectorFileWriter *out) +{ + SBezierLoopSetSet sblss = {}; + SEdge *se; + for(se = sel->l.First(); se; se = sel->l.NextAfter(se)) { + SBezier sb = SBezier::From( + (se->a).ScaledBy(1.0 / SS.exportScale), + (se->b).ScaledBy(1.0 / SS.exportScale)); + sblss.AddOpenPath(&sb); + } + + sbl->ScaleSelfBy(1.0/SS.exportScale); + SBezier *sb; + for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) { + sblss.AddOpenPath(sb); + } + + out->OutputLinesAndMesh(&sblss, NULL); + sblss.Clear(); +} + +void SolveSpaceUI::ExportLinesAndMesh(SEdgeList *sel, SBezierList *sbl, SMesh *sm, + Vector u, Vector v, Vector n, + Vector origin, double cameraTan, + VectorFileWriter *out) +{ + double s = 1.0 / SS.exportScale; + + // Project into the export plane; so when we're done, z doesn't matter, + // and x and y are what goes in the DXF. + for(SEdge *e = sel->l.First(); e; e = sel->l.NextAfter(e)) { + // project into the specified csys, and apply export scale + (e->a) = e->a.InPerspective(u, v, n, origin, cameraTan).ScaledBy(s); + (e->b) = e->b.InPerspective(u, v, n, origin, cameraTan).ScaledBy(s); + } + + if(sbl) { + for(SBezier *b = sbl->l.First(); b; b = sbl->l.NextAfter(b)) { + *b = b->InPerspective(u, v, n, origin, cameraTan); + int i; + for(i = 0; i <= b->deg; i++) { + b->ctrl[i] = (b->ctrl[i]).ScaledBy(s); + } + } + } + + // If cutter radius compensation is requested, then perform it now + if(fabs(SS.exportOffset) > LENGTH_EPS) { + // assemble those edges into a polygon, and clear the edge list + SPolygon sp = {}; + sel->AssemblePolygon(&sp, NULL); + sel->Clear(); + + SPolygon compd = {}; + sp.normal = Vector::From(0, 0, -1); + sp.FixContourDirections(); + sp.OffsetInto(&compd, SS.exportOffset*s); + sp.Clear(); + + compd.MakeEdgesInto(sel); + compd.Clear(); + } + + // Now the triangle mesh; project, then build a BSP to perform + // occlusion testing and generated the shaded surfaces. + SMesh smp = {}; + if(sm) { + Vector l0 = (SS.lightDir[0]).WithMagnitude(1), + l1 = (SS.lightDir[1]).WithMagnitude(1); + STriangle *tr; + for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) { + STriangle tt = *tr; + tt.a = (tt.a).InPerspective(u, v, n, origin, cameraTan).ScaledBy(s); + tt.b = (tt.b).InPerspective(u, v, n, origin, cameraTan).ScaledBy(s); + tt.c = (tt.c).InPerspective(u, v, n, origin, cameraTan).ScaledBy(s); + + // And calculate lighting for the triangle + Vector n = tt.Normal().WithMagnitude(1); + double lighting = min(1.0, SS.ambientIntensity + + max(0.0, (SS.lightIntensity[0])*(n.Dot(l0))) + + max(0.0, (SS.lightIntensity[1])*(n.Dot(l1)))); + double r = min(1.0, tt.meta.color.redF() * lighting), + g = min(1.0, tt.meta.color.greenF() * lighting), + b = min(1.0, tt.meta.color.blueF() * lighting); + tt.meta.color = RGBf(r, g, b); + smp.AddTriangle(&tt); + } + } + + SMesh sms = {}; + + // We need the mesh for occlusion testing, but if we don't/can't export it, + // don't generate it. + if(SS.GW.showShaded && out->CanOutputMesh()) { + // Use the BSP routines to generate the split triangles in paint order. + SBsp3 *bsp = SBsp3::FromMesh(&smp); + if(bsp) bsp->GenerateInPaintOrder(&sms); + // And cull the back-facing triangles + STriangle *tr; + sms.l.ClearTags(); + for(tr = sms.l.First(); tr; tr = sms.l.NextAfter(tr)) { + Vector n = tr->Normal(); + if(n.z < 0) { + tr->tag = 1; + } + } + sms.l.RemoveTagged(); + } + + // And now we perform hidden line removal if requested + SEdgeList hlrd = {}; + if(sm) { + SKdNode *root = SKdNode::From(&smp); + + // Generate the edges where a curved surface turns from front-facing + // to back-facing. + if(SS.GW.showEdges || SS.GW.showOutlines) { + root->MakeCertainEdgesInto(sel, EdgeKind::TURNING, + /*coplanarIsInter=*/false, NULL, NULL, + GW.showOutlines ? Style::OUTLINE : Style::SOLID_EDGE); + } + + root->ClearTags(); + int cnt = 1234; + + SEdge *se; + for(se = sel->l.First(); se; se = sel->l.NextAfter(se)) { + if(se->auxA == Style::CONSTRAINT) { + // Constraints should not get hidden line removed; they're + // always on top. + hlrd.AddEdge(se->a, se->b, se->auxA); + continue; + } + + SEdgeList edges = {}; + // Split the original edge against the mesh + edges.AddEdge(se->a, se->b, se->auxA); + root->OcclusionTestLine(*se, &edges, cnt); + if(SS.GW.drawOccludedAs == GraphicsWindow::DrawOccludedAs::STIPPLED) { + for(SEdge &se : edges.l) { + if(se.tag == 1) { + se.auxA = Style::HIDDEN_EDGE; + } + } + } else if(SS.GW.drawOccludedAs == GraphicsWindow::DrawOccludedAs::INVISIBLE) { + edges.l.RemoveTagged(); + } + + // the occlusion test splits unnecessarily; so fix those + edges.MergeCollinearSegments(se->a, se->b); + cnt++; + // And add the results to our output + SEdge *sen; + for(sen = edges.l.First(); sen; sen = edges.l.NextAfter(sen)) { + hlrd.AddEdge(sen->a, sen->b, sen->auxA); + } + edges.Clear(); + } + + sel = &hlrd; + } + + // Clean up: remove overlapping line segments and + // segments with zero-length projections. + sel->l.ClearTags(); + for(int i = 0; i < sel->l.n; ++i) { + SEdge *sei = &sel->l[i]; + hStyle hsi = { (uint32_t)sei->auxA }; + Style *si = Style::Get(hsi); + if(sei->tag != 0) continue; + + // Remove segments with zero length projections. + Vector ai = sei->a; + ai.z = 0.0; + Vector bi = sei->b; + bi.z = 0.0; + Vector di = bi.Minus(ai); + if(fabs(di.x) < LENGTH_EPS && fabs(di.y) < LENGTH_EPS) { + sei->tag = 1; + continue; + } + + for(int j = i + 1; j < sel->l.n; ++j) { + SEdge *sej = &sel->l[j]; + if(sej->tag != 0) continue; + + Vector *pAj = &sej->a; + Vector *pBj = &sej->b; + + // Remove segments with zero length projections. + Vector aj = sej->a; + aj.z = 0.0; + Vector bj = sej->b; + bj.z = 0.0; + Vector dj = bj.Minus(aj); + if(fabs(dj.x) < LENGTH_EPS && fabs(dj.y) < LENGTH_EPS) { + sej->tag = 1; + continue; + } + + // Skip non-collinear segments. + const double eps = 1e-6; + if(aj.DistanceToLine(ai, di) > eps) continue; + if(bj.DistanceToLine(ai, di) > eps) continue; + + double ta = aj.Minus(ai).Dot(di) / di.Dot(di); + double tb = bj.Minus(ai).Dot(di) / di.Dot(di); + if(ta > tb) { + std::swap(pAj, pBj); + std::swap(ta, tb); + } + + hStyle hsj = { (uint32_t)sej->auxA }; + Style *sj = Style::Get(hsj); + + bool canRemoveI = sej->auxA == sei->auxA || si->zIndex < sj->zIndex; + bool canRemoveJ = sej->auxA == sei->auxA || sj->zIndex < si->zIndex; + + if(canRemoveJ) { + // j-segment inside i-segment + if(ta > 0.0 - eps && tb < 1.0 + eps) { + sej->tag = 1; + continue; + } + + // cut segment + bool aInside = ta > 0.0 - eps && ta < 1.0 + eps; + if(tb > 1.0 - eps && aInside) { + *pAj = sei->b; + continue; + } + + // cut segment + bool bInside = tb > 0.0 - eps && tb < 1.0 + eps; + if(ta < 0.0 - eps && bInside) { + *pBj = sei->a; + continue; + } + + // split segment + if(ta < 0.0 - eps && tb > 1.0 + eps) { + sel->AddEdge(sei->b, *pBj, sej->auxA, sej->auxB); + *pBj = sei->a; + continue; + } + } + + if(canRemoveI) { + // j-segment inside i-segment + if(ta < 0.0 + eps && tb > 1.0 - eps) { + sei->tag = 1; + break; + } + + // cut segment + bool aInside = ta > 0.0 + eps && ta < 1.0 - eps; + if(tb > 1.0 - eps && aInside) { + sei->b = *pAj; + i--; + break; + } + + // cut segment + bool bInside = tb > 0.0 + eps && tb < 1.0 - eps; + if(ta < 0.0 + eps && bInside) { + sei->a = *pBj; + i--; + break; + } + + // split segment + if(ta > 0.0 + eps && tb < 1.0 - eps) { + sel->AddEdge(*pBj, sei->b, sei->auxA, sei->auxB); + sei->b = *pAj; + i--; + break; + } + } + } + } + sel->l.RemoveTagged(); + + // We kept the line segments and Beziers separate until now; but put them + // all together, and also project everything into the xy plane, since not + // all export targets ignore the z component of the points. + ssassert(sbl != nullptr, "Adding line segments to beziers assumes bezier list is non-null."); + for(SEdge *e = sel->l.First(); e; e = sel->l.NextAfter(e)) { + SBezier sb = SBezier::From(e->a, e->b); + sb.auxA = e->auxA; + sbl->l.Add(&sb); + } + for(SBezier *b = sbl->l.First(); b; b = sbl->l.NextAfter(b)) { + for(int i = 0; i <= b->deg; i++) { + b->ctrl[i].z = 0; + } + } + + // If possible, then we will assemble these output curves into loops. They + // will then get exported as closed paths. + SBezierLoopSetSet sblss = {}; + SBezierLoopSet leftovers = {}; + SSurface srf = SSurface::FromPlane(Vector::From(0, 0, 0), + Vector::From(1, 0, 0), + Vector::From(0, 1, 0)); + SPolygon spxyz = {}; + bool allClosed; + SEdge notClosedAt; + sbl->l.ClearTags(); + sblss.FindOuterFacesFrom(sbl, &spxyz, &srf, + SS.ExportChordTolMm(), + &allClosed, ¬ClosedAt, + NULL, NULL, + &leftovers); + sblss.l.Add(&leftovers); + + // Now write the lines and triangles to the output file + out->OutputLinesAndMesh(&sblss, &sms); + + spxyz.Clear(); + sblss.Clear(); + smp.Clear(); + sms.Clear(); + hlrd.Clear(); +} + +double VectorFileWriter::MmToPts(double mm) { + // 72 points in an inch + return (mm/25.4)*72; +} + +VectorFileWriter *VectorFileWriter::ForFile(const Platform::Path &filename) { + VectorFileWriter *ret; + bool needOpen = true; + if(filename.HasExtension("dxf")) { + static DxfFileWriter DxfWriter; + ret = &DxfWriter; + needOpen = false; + } else if(filename.HasExtension("ps") || filename.HasExtension("eps")) { + static EpsFileWriter EpsWriter; + ret = &EpsWriter; + } else if(filename.HasExtension("pdf")) { + static PdfFileWriter PdfWriter; + ret = &PdfWriter; + } else if(filename.HasExtension("svg")) { + static SvgFileWriter SvgWriter; + ret = &SvgWriter; + } else if(filename.HasExtension("plt") || filename.HasExtension("hpgl")) { + static HpglFileWriter HpglWriter; + ret = &HpglWriter; + } else if(filename.HasExtension("step") || filename.HasExtension("stp")) { + static Step2dFileWriter Step2dWriter; + ret = &Step2dWriter; + } else if(filename.HasExtension("txt") || filename.HasExtension("ngc")) { + static GCodeFileWriter GCodeWriter; + ret = &GCodeWriter; + } else { + Error("Can't identify output file type from file extension of " + "filename '%s'; try " + ".step, .stp, .dxf, .svg, .plt, .hpgl, .pdf, .txt, .ngc, " + ".eps, or .ps.", + filename.raw.c_str()); + return NULL; + } + ret->filename = filename; + if(!needOpen) return ret; + + FILE *f = OpenFile(filename, "wb"); + if(!f) { + Error("Couldn't write to '%s'", filename.raw.c_str()); + return NULL; + } + ret->f = f; + return ret; +} + +void VectorFileWriter::SetModelviewProjection(const Vector &u, const Vector &v, const Vector &n, + const Vector &origin, double cameraTan, + double scale) { + this->u = u; + this->v = v; + this->n = n; + this->origin = origin; + this->cameraTan = cameraTan; + this->scale = scale; +} + +Vector VectorFileWriter::Transform(Vector &pos) const { + return pos.InPerspective(u, v, n, origin, cameraTan).ScaledBy(1.0 / scale); +} + +void VectorFileWriter::OutputLinesAndMesh(SBezierLoopSetSet *sblss, SMesh *sm) { + STriangle *tr; + SBezier *b; + + // First calculate the bounding box. + ptMin = Vector::From(VERY_POSITIVE, VERY_POSITIVE, VERY_POSITIVE); + ptMax = Vector::From(VERY_NEGATIVE, VERY_NEGATIVE, VERY_NEGATIVE); + if(sm) { + for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) { + (tr->a).MakeMaxMin(&ptMax, &ptMin); + (tr->b).MakeMaxMin(&ptMax, &ptMin); + (tr->c).MakeMaxMin(&ptMax, &ptMin); + } + } + if(sblss) { + SBezierLoopSet *sbls; + for(sbls = sblss->l.First(); sbls; sbls = sblss->l.NextAfter(sbls)) { + SBezierLoop *sbl; + for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) { + for(b = sbl->l.First(); b; b = sbl->l.NextAfter(b)) { + for(int i = 0; i <= b->deg; i++) { + (b->ctrl[i]).MakeMaxMin(&ptMax, &ptMin); + } + } + } + } + } + + // And now we compute the canvas size. + double s = 1.0 / SS.exportScale; + if(SS.exportCanvasSizeAuto) { + // It's based on the calculated bounding box; we grow it along each + // boundary by the specified amount. + ptMin.x -= s*SS.exportMargin.left; + ptMax.x += s*SS.exportMargin.right; + ptMin.y -= s*SS.exportMargin.bottom; + ptMax.y += s*SS.exportMargin.top; + } else { + ptMin.x = (s*SS.exportCanvas.dx); + ptMin.y = (s*SS.exportCanvas.dy); + ptMax.x = ptMin.x + (s*SS.exportCanvas.width); + ptMax.y = ptMin.y + (s*SS.exportCanvas.height); + } + + StartFile(); + if(SS.exportBackgroundColor) { + Background(SS.backgroundColor); + } + if(sm && SS.exportShadedTriangles) { + for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) { + Triangle(tr); + } + } + if(sblss) { + SBezierLoopSet *sbls; + for(sbls = sblss->l.First(); sbls; sbls = sblss->l.NextAfter(sbls)) { + for(SBezierLoop *sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) { + b = sbl->l.First(); + if(!b || !Style::Exportable(b->auxA)) continue; + + hStyle hs = { (uint32_t)b->auxA }; + Style *stl = Style::Get(hs); + double lineWidth = Style::WidthMm(b->auxA)*s; + RgbaColor strokeRgb = Style::Color(hs, /*forExport=*/true); + RgbaColor fillRgb = Style::FillColor(hs, /*forExport=*/true); + + StartPath(strokeRgb, lineWidth, stl->filled, fillRgb, hs); + for(b = sbl->l.First(); b; b = sbl->l.NextAfter(b)) { + Bezier(b); + } + FinishPath(strokeRgb, lineWidth, stl->filled, fillRgb, hs); + } + } + } + FinishAndCloseFile(); +} + +void VectorFileWriter::BezierAsPwl(SBezier *sb) { + List lv = {}; + sb->MakePwlInto(&lv, SS.ExportChordTolMm()); + + for(int i = 1; i < lv.n; i++) { + SBezier sb = SBezier::From(lv[i-1], lv[i]); + Bezier(&sb); + } + lv.Clear(); +} + +void VectorFileWriter::BezierAsNonrationalCubic(SBezier *sb, int depth) { + Vector t0 = sb->TangentAt(0), t1 = sb->TangentAt(1); + // The curve is correct, and the first derivatives are correct, at the + // endpoints. + SBezier bnr = SBezier::From( + sb->Start(), + sb->Start().Plus(t0.ScaledBy(1.0/3)), + sb->Finish().Minus(t1.ScaledBy(1.0/3)), + sb->Finish()); + + double tol = SS.ExportChordTolMm(); + // Arbitrary choice, but make it a little finer than pwl tolerance since + // it should be easier to achieve that with the smooth curves. + tol /= 2; + + bool closeEnough = true; + int i; + for(i = 1; i <= 3; i++) { + double t = i/4.0; + Vector p0 = sb->PointAt(t), + pn = bnr.PointAt(t); + double d = (p0.Minus(pn)).Magnitude(); + if(d > tol) { + closeEnough = false; + } + } + + if(closeEnough || depth > 3) { + Bezier(&bnr); + } else { + SBezier bef, aft; + sb->SplitAt(0.5, &bef, &aft); + BezierAsNonrationalCubic(&bef, depth+1); + BezierAsNonrationalCubic(&aft, depth+1); + } +} + +//----------------------------------------------------------------------------- +// Export a triangle mesh, in the requested format. +//----------------------------------------------------------------------------- +void SolveSpaceUI::ExportMeshTo(const Platform::Path &filename) { + SS.exportMode = true; + GenerateAll(Generate::ALL); + + Group *g = SK.GetGroup(SS.GW.activeGroup); + g->GenerateDisplayItems(); + + SMesh *m = &(SK.GetGroup(SS.GW.activeGroup)->displayMesh); + if(m->IsEmpty()) { + Error(_("Active group mesh is empty; nothing to export.")); + return; + } + + FILE *f = OpenFile(filename, "wb"); + if(!f) { + Error("Couldn't write to '%s'", filename.raw.c_str()); + return; + } + ShowNakedEdges(/*reportOnlyWhenNotOkay=*/true); + if(filename.HasExtension("stl")) { + ExportMeshAsStlTo(f, m); + } else if(filename.HasExtension("obj")) { + Platform::Path mtlFilename = filename.WithExtension("mtl"); + FILE *fMtl = OpenFile(mtlFilename, "wb"); + if(!fMtl) { + Error("Couldn't write to '%s'", filename.raw.c_str()); + return; + } + + fprintf(f, "mtllib %s\n", mtlFilename.FileName().c_str()); + ExportMeshAsObjTo(f, fMtl, m); + + fclose(fMtl); + } else if(filename.HasExtension("js") || + filename.HasExtension("html")) { + SOutlineList *e = &(SK.GetGroup(SS.GW.activeGroup)->displayOutlines); + ExportMeshAsThreeJsTo(f, filename, m, e); + } else if(filename.HasExtension("wrl")) { + ExportMeshAsVrmlTo(f, filename, m); + } else { + Error("Can't identify output file type from file extension of " + "filename '%s'; try .stl, .obj, .js, .html.", filename.raw.c_str()); + } + + fclose(f); + + SS.justExportedInfo.showOrigin = false; + SS.justExportedInfo.draw = true; + GW.Invalidate(); +} + +//----------------------------------------------------------------------------- +// Export the mesh as an STL file; it should always be vertex-to-vertex and +// not self-intersecting, so not much to do. +//----------------------------------------------------------------------------- +void SolveSpaceUI::ExportMeshAsStlTo(FILE *f, SMesh *sm) { + char str[80] = {}; + strcpy(str, "STL exported mesh"); + fwrite(str, 1, 80, f); + + uint32_t n = sm->l.n; + fwrite(&n, 4, 1, f); + + double s = SS.exportScale; + int i; + for(i = 0; i < sm->l.n; i++) { + STriangle *tr = &(sm->l[i]); + Vector n = tr->Normal().WithMagnitude(1); + float w; + w = (float)n.x; fwrite(&w, 4, 1, f); + w = (float)n.y; fwrite(&w, 4, 1, f); + w = (float)n.z; fwrite(&w, 4, 1, f); + w = (float)((tr->a.x)/s); fwrite(&w, 4, 1, f); + w = (float)((tr->a.y)/s); fwrite(&w, 4, 1, f); + w = (float)((tr->a.z)/s); fwrite(&w, 4, 1, f); + w = (float)((tr->b.x)/s); fwrite(&w, 4, 1, f); + w = (float)((tr->b.y)/s); fwrite(&w, 4, 1, f); + w = (float)((tr->b.z)/s); fwrite(&w, 4, 1, f); + w = (float)((tr->c.x)/s); fwrite(&w, 4, 1, f); + w = (float)((tr->c.y)/s); fwrite(&w, 4, 1, f); + w = (float)((tr->c.z)/s); fwrite(&w, 4, 1, f); + fputc(0, f); + fputc(0, f); + } +} + +//----------------------------------------------------------------------------- +// Export the mesh as Wavefront OBJ format. This requires us to reduce all the +// identical vertices to the same identifier, so do that first. +//----------------------------------------------------------------------------- +void SolveSpaceUI::ExportMeshAsObjTo(FILE *fObj, FILE *fMtl, SMesh *sm) { + std::map colors; + for(const STriangle &t : sm->l) { + RgbaColor color = t.meta.color; + if(colors.find(color) == colors.end()) { + std::string id = ssprintf("h%02x%02x%02x", + color.red, + color.green, + color.blue); + colors.emplace(color, id); + } + for(int i = 0; i < 3; i++) { + fprintf(fObj, "v %.10f %.10f %.10f\n", + CO(t.vertices[i].ScaledBy(1 / SS.exportScale))); + } + } + + for(auto &it : colors) { + fprintf(fMtl, "newmtl %s\n", + it.second.c_str()); + fprintf(fMtl, "Kd %.3f %.3f %.3f\n", + it.first.redF(), it.first.greenF(), it.first.blueF()); + } + + for(const STriangle &t : sm->l) { + for(int i = 0; i < 3; i++) { + Vector n = t.normals[i].WithMagnitude(1.0); + fprintf(fObj, "vn %.10f %.10f %.10f\n", + CO(n)); + } + } + + RgbaColor currentColor = {}; + for(int i = 0; i < sm->l.n; i++) { + const STriangle &t = sm->l[i]; + if(!currentColor.Equals(t.meta.color)) { + currentColor = t.meta.color; + fprintf(fObj, "usemtl %s\n", colors[currentColor].c_str()); + } + + fprintf(fObj, "f %d//%d %d//%d %d//%d\n", + i * 3 + 1, i * 3 + 1, + i * 3 + 2, i * 3 + 2, + i * 3 + 3, i * 3 + 3); + } +} + +//----------------------------------------------------------------------------- +// Export the mesh as a JavaScript script, which is compatible with Three.js. +//----------------------------------------------------------------------------- +void SolveSpaceUI::ExportMeshAsThreeJsTo(FILE *f, const Platform::Path &filename, + SMesh *sm, SOutlineList *sol) +{ + SPointList spl = {}; + STriangle *tr; + Vector bndl, bndh; + + const std::string THREE_FN("three-r111.min.js"); + const std::string HAMMER_FN("hammer-2.0.8.js"); + const std::string CONTROLS_FN("SolveSpaceControls.js"); + + const char htmlbegin[] = R"( + + + + + Three.js Solvespace Mesh + + + + + + + + + +)"; + + // A default three.js viewer with OrthographicTrackballControls is + // generated as a comment preceding the data. + + // x bounds should be the range of x or y, whichever + // is larger, before aspect ratio correction is applied. + // y bounds should be the range of x or y, whichever is + // larger. No aspect ratio correction is applied. + // Near plane should be 1. + // Camera's z-position should be the range of z + 1 or the larger of + // the x or y bounds, whichever is larger. + // Far plane should be at least twice as much as the camera's + // z-position. + // Edge projection bias should be about 1/500 of the far plane's distance. + // Further corrections will be applied to the z-position and far plane in + // the default viewer, but the defaults are fine for a model which + // only rotates about the world origin. + + sm->GetBounding(&bndh, &bndl); + double largerBoundXY = max((bndh.x - bndl.x), (bndh.y - bndl.y)); + double largerBoundZ = max(largerBoundXY, (bndh.z - bndl.z + 1)); + + std::string basename = filename.FileStem(); + for(size_t i = 0; i < basename.length(); i++) { + if(!(isalnum(basename[i]) || ((unsigned)basename[i] >= 0x80))) { + basename[i] = '_'; + } + } + + if(filename.HasExtension("html")) { + fprintf(f, htmlbegin, + THREE_FN.c_str(), + LoadStringFromGzip("threejs/" + THREE_FN + ".gz").c_str(), + HAMMER_FN.c_str(), + LoadStringFromGzip("threejs/" + HAMMER_FN + ".gz").c_str(), + CONTROLS_FN.c_str(), + LoadString("threejs/" + CONTROLS_FN).c_str()); + } + + fprintf(f, "var solvespace_model_%s = {\n" + " bounds: {\n" + " x: %f, y: %f, near: %f, far: %f, z: %f, edgeBias: %f\n" + " },\n", + basename.c_str(), + largerBoundXY, + largerBoundXY, + 1.0, + largerBoundZ * 2, + largerBoundZ, + largerBoundZ / 250); + + // Output lighting information. + fputs(" lights: {\n" + " d: [\n", f); + + // Directional. + int lightCount; + for(lightCount = 0; lightCount < 2; lightCount++) { + fprintf(f, " {\n" + " intensity: %f, direction: [%f, %f, %f]\n" + " },\n", + SS.lightIntensity[lightCount], + CO(SS.lightDir[lightCount])); + } + + // Global Ambience. + fprintf(f, " ],\n" + " a: %f\n", SS.ambientIntensity); + + for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) { + spl.IncrementTagFor(tr->a); + spl.IncrementTagFor(tr->b); + spl.IncrementTagFor(tr->c); + } + + // Output all the vertices. + SPoint *sp; + fputs(" },\n" + " points: [\n", f); + for(sp = spl.l.First(); sp; sp = spl.l.NextAfter(sp)) { + fprintf(f, " [%f, %f, %f],\n", + sp->p.x / SS.exportScale, + sp->p.y / SS.exportScale, + sp->p.z / SS.exportScale); + } + + fputs(" ],\n" + " faces: [\n", f); + // And now all the triangular faces, in terms of those vertices. + // This time we count from zero. + for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) { + fprintf(f, " [%d, %d, %d],\n", + spl.IndexForPoint(tr->a), + spl.IndexForPoint(tr->b), + spl.IndexForPoint(tr->c)); + } + + // Output face normals. + fputs(" ],\n" + " normals: [\n", f); + for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) { + fprintf(f, " [[%f, %f, %f], [%f, %f, %f], [%f, %f, %f]],\n", + CO(tr->an), CO(tr->bn), CO(tr->cn)); + } + + fputs(" ],\n" + " colors: [\n", f); + // Output triangle colors. + for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) { + fprintf(f, " 0x%x,\n", tr->meta.color.ToARGB32()); + } + + fputs(" ],\n" + " edges: [\n", f); + // Output edges. Assume user's model colors do not obscure white edges. + for(const SOutline &so : sol->l) { + if(so.tag == 0) continue; + fprintf(f, " [[%f, %f, %f], [%f, %f, %f]],\n", + so.a.x / SS.exportScale, + so.a.y / SS.exportScale, + so.a.z / SS.exportScale, + so.b.x / SS.exportScale, + so.b.y / SS.exportScale, + so.b.z / SS.exportScale); + } + + fputs(" ]\n};\n", f); + + if(filename.HasExtension("html")) { + fprintf(f, htmlend, + basename.c_str(), + SS.GW.scale, + CO(SS.GW.offset), + CO(SS.GW.projUp), + CO(SS.GW.projRight)); + } + + spl.Clear(); +} + +//----------------------------------------------------------------------------- +// Export the mesh as a VRML text file / WRL. +//----------------------------------------------------------------------------- +void SolveSpaceUI::ExportMeshAsVrmlTo(FILE *f, const Platform::Path &filename, SMesh *sm) { + struct STriangleSpan { + STriangle *first, *past_last; + + STriangle *begin() const { return first; } + STriangle *end() const { return past_last; } + }; + + + std::string basename = filename.FileStem(); + for(auto & c : basename) { + if(!(isalnum(c) || ((unsigned)c >= 0x80))) { + c = '_'; + } + } + + fprintf(f, "#VRML V2.0 utf8\n" + "#Exported from SolveSpace %s\n" + "\n" + "DEF %s Transform {\n" + " children [", + PACKAGE_VERSION, + basename.c_str()); + + + std::map> opacities; + STriangle *start = sm->l.begin(); + std::uint8_t last_opacity = start->meta.color.alpha; + for(auto & tr : sm->l) { + if(tr.meta.color.alpha != last_opacity) { + opacities[last_opacity].push_back(STriangleSpan{start, &tr}); + start = &tr; + last_opacity = start->meta.color.alpha; + } + } + opacities[last_opacity].push_back(STriangleSpan{start, sm->l.end()}); + + for(auto && op : opacities) { + fprintf(f, "\n" + " Shape {\n" + " appearance Appearance {\n" + " material DEF %s_material_%u Material {\n" + " diffuseColor %f %f %f\n" + " ambientIntensity %f\n" + " transparency %f\n" + " }\n" + " }\n" + " geometry IndexedFaceSet {\n" + " colorPerVertex TRUE\n" + " coord Coordinate { point [\n", + basename.c_str(), + (unsigned)op.first, + SS.ambientIntensity, + SS.ambientIntensity, + SS.ambientIntensity, + SS.ambientIntensity, + 1.f - ((float)op.first / 255.0f)); + + SPointList spl = {}; + + for(const auto & sp : op.second) { + for(const auto & tr : sp) { + spl.IncrementTagFor(tr.a); + spl.IncrementTagFor(tr.b); + spl.IncrementTagFor(tr.c); + } + } + + // Output all the vertices. + for(const auto &sp : spl.l) { + fprintf(f, " %f %f %f,\n", + sp.p.x / SS.exportScale, + sp.p.y / SS.exportScale, + sp.p.z / SS.exportScale); + } + + fputs(" ] }\n" + " coordIndex [\n", f); + // And now all the triangular faces, in terms of those vertices. + for(const auto & sp : op.second) { + for(const auto & tr : sp) { + fprintf(f, " %d, %d, %d, -1,\n", + spl.IndexForPoint(tr.a), + spl.IndexForPoint(tr.b), + spl.IndexForPoint(tr.c)); + } + } + + fputs(" ]\n" + " color Color { color [\n", f); + // Output triangle colors. + std::vector triangle_colour_ids; + std::vector colours_present; + for(const auto & sp : op.second) { + for(const auto & tr : sp) { + const auto colour_itr = std::find_if(colours_present.begin(), colours_present.end(), + [&](const RgbaColor & c) { + return c.Equals(tr.meta.color); + }); + if(colour_itr == colours_present.end()) { + fprintf(f, " %.10f %.10f %.10f,\n", + tr.meta.color.redF(), + tr.meta.color.greenF(), + tr.meta.color.blueF()); + triangle_colour_ids.push_back(colours_present.size()); + colours_present.insert(colours_present.end(), tr.meta.color); + } else { + triangle_colour_ids.push_back(colour_itr - colours_present.begin()); + } + } + } + + fputs(" ] }\n" + " colorIndex [\n", f); + + for(auto colour_idx : triangle_colour_ids) { + fprintf(f, " %d, %d, %d, -1,\n", colour_idx, colour_idx, colour_idx); + } + + fputs(" ]\n" + " }\n" + " }\n", f); + + spl.Clear(); + } + + fputs(" ]\n" + "}\n", f); +} + +//----------------------------------------------------------------------------- +// Export a view of the model as an image; we just take a screenshot, by +// rendering the view in the usual way and then copying the pixels. +//----------------------------------------------------------------------------- +void SolveSpaceUI::ExportAsPngTo(const Platform::Path &filename) { + screenshotFile = filename; + // The rest of the work is done in the next redraw. + GW.Invalidate(); +} + +} // namespace SolveSpace diff --git a/src/exportstep.cpp b/src/exportstep.cpp new file mode 100644 index 0000000..6bfd974 --- /dev/null +++ b/src/exportstep.cpp @@ -0,0 +1,721 @@ +//----------------------------------------------------------------------------- +// Export a STEP file describing our ratpoly shell. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +#define FP "%.5f" // Floating Point coordinate precision. Since LENGTH_EPS = 1e-6 output 5 decimal places thus rounding out errors e.g. 0.999999mm +#define CARTESIAN_POINT_FORMAT "#%d=CARTESIAN_POINT('',(" FP "," FP "," FP "));\n" +const double PRECISION = 2*LENGTH_EPS; + +//----------------------------------------------------------------------------- +// Functions for STEP export: duplication check. +//----------------------------------------------------------------------------- +// Check if this point was already defined with a different ID number. +// inputs: +// number -> id of the cartesian point +// v -> position of the cartesian point +// vertex -> id of the vertex linked to this point (<1 if none) +// return: +// true, if the cartesian point is already defined +bool StepFileWriter::HasCartesianPointAnAlias(int number, Vector v, int vertex, bool *vertex_has_alias) { + // Look for this point "v" in the alias list. + for(pointAliases_t &p : pointAliases) { + if(p.v.Equals(v, PRECISION)) { + // This point was already defined. + // The new number is just an alias. + p.alias.aliases.push_back(number); + if(vertex > 0) { + p.vertexAlias.reference = (p.vertexAlias.reference <= 0 ? + vertex : p.vertexAlias.reference); + p.vertexAlias.aliases.push_back(vertex); + + // If the caller is interested let them know if the vertex is new or has an existing alias. + if(nullptr != vertex_has_alias) { + if(p.vertexAlias.aliases.size() == 1) { + // This is a new vertex. + *vertex_has_alias = false; + } else { + *vertex_has_alias = true; + } + } + } + return true; + } + } + + // No point was found, it means this is a new point. + pointAliases_t newPoint; + newPoint.alias.reference = number; + newPoint.alias.aliases.push_back(number); + newPoint.v = v; + newPoint.vertexAlias.reference = vertex; + if(vertex > 0) { + newPoint.vertexAlias.aliases.push_back(vertex); + } + + // A new entry in the list. + pointAliases.push_back(newPoint); + return false; +} + +// Return a cartesian point index; if the point has aliases return the +// first one. +// input: +// number -> the id of the requested cartesian point +// return: +// number, if the point has no aliases, otherwise its first alias +int StepFileWriter::InsertPoint(int number) { + // Look for a point with index "number" in the list of aliases. + for(pointAliases_t p : pointAliases) { + for(int alias : p.alias.aliases) { + if(alias == number) { + return p.alias.reference; + } + } + } + + // ERROR: it should never reach this point... + return -1; +} + +// Return a vertex index; if the vertex has aliases return the +// first one. +// input: +// number -> the id of the requested vertex +// return: +// number, if the vertex has no aliases, otherwise its first alias +int StepFileWriter::InsertVertex(int number) { + // Look for a point with index "number" in the list of vertex aliases. + for(pointAliases_t p : pointAliases) { + for(int alias : p.vertexAlias.aliases) { + if(alias == number) { + return p.vertexAlias.reference; + } + } + } + + // ERROR: it should never reach this point... + return -1; +} + +// Check whether this curve was already defined with a different ID number. +// inputs: +// number -> id of the curve +// points -> list of points that define the curve +// return: +// true, if the curve is already defined +bool StepFileWriter::HasBSplineCurveAnAlias(int number, std::vector points) { + // Look for this curve in the alias list. + for(curveAliases_t &c : curveAliases) { + if(points.size() != c.memberPoints.size()) { + continue; + } if(exportParts && !(c.color.Equals(currentColor))) { + continue; + } else { + size_t matches = 0; // is this the same curve? + // FIXME: this hack should work _most_ of the times + for(size_t i = 0; i < points.size(); i++) { + for(size_t j = 0; j < points.size(); j++) { + if(points[i] == c.memberPoints[j]) { + matches++; + } + } + } + + if(matches == points.size()) { + // Add this alias. + c.alias.aliases.push_back(number); + return true; + } + } + } + + // No curve was found, it means this is a new curve. + curveAliases_t newCurve; + newCurve.alias.reference = number; + newCurve.alias.aliases.push_back(number); + newCurve.memberPoints = points; + newCurve.color = currentColor; + + // A new entry in the list. + curveAliases.push_back(newCurve); + return false; +} + +// Return a curve index. As above. +int StepFileWriter::InsertCurve(int number) { + for(curveAliases_t c : curveAliases) { + for(int alias : c.alias.aliases) { + if(alias == number && (!exportParts || c.color.Equals(currentColor))) { + return c.alias.reference; + } + } + } + + // ERROR: it should never reach this point... + return -1; +} + +// Check whether this edge curve was already defined with a different ID number. +// inputs: +// number -> id of the edge +// prevFinish, thisFinish -> points of the edge +// curveID -> curve of the edge +// flip -> if an edge curve already exists this indicates whether it is in the other direction +// return: +// true, if the edge curve is already defined +bool StepFileWriter::HasEdgeCurveAnAlias(int number, int prevFinish, int thisFinish, int curveId, bool *flip) { + // Look for this edge in the alias list. + for(edgeCurveAliases_t &e : edgeCurveAliases) { + if(exportParts && !(e.color.Equals(currentColor))) { + continue; + } + // Check both directions. + if(((prevFinish == e.prevFinish && thisFinish == e.thisFinish) || + (prevFinish == e.thisFinish && thisFinish == e.prevFinish)) && + curveId == e.curveId) { + e.alias.aliases.push_back(number); + if(nullptr != flip) { + if(prevFinish == e.thisFinish && thisFinish == e.prevFinish) + *flip = true; + else + *flip = false; + } + return true; + } + } + + // New edge curve. + edgeCurveAliases_t newEdgeCurve; + newEdgeCurve.alias.reference = number; + newEdgeCurve.alias.aliases.push_back(number); + newEdgeCurve.prevFinish = prevFinish; + newEdgeCurve.thisFinish = thisFinish; + newEdgeCurve.curveId = curveId; + newEdgeCurve.color = currentColor; + + edgeCurveAliases.push_back(newEdgeCurve); + return false; +} + +// Return an edge curve index. As above. +int StepFileWriter::InsertEdgeCurve(int number) { + for(edgeCurveAliases_t e : edgeCurveAliases) { + for(int alias : e.alias.aliases) { + if(alias == number && (!exportParts || e.color.Equals(currentColor))) { + return e.alias.reference; + } + } + } + + // ERROR: it should never reach this point... + return -1; +} + +// Check whether this oriented edge curve was already defined with a different ID number. +// inputs: +// number -> id of the edge +// edgeCurveId -> curve of the edge +// flip -> the direction of the oriented edge +// return: +// true, if the oriented edge is already defined +bool StepFileWriter::HasOrientedEdgeAnAlias(int number, int edgeCurveId, bool flip) { + // Look for this edge in the alias list. + for(orientedEdgeAliases_t &e : orientedEdgeAliases) { + if((edgeCurveId == e.edgeCurveId) && (flip == e.flip)) { + e.alias.aliases.push_back(number); + return true; + } + } + + // New oriented edge. + orientedEdgeAliases_t newOrientedEdge; + newOrientedEdge.alias.reference = number; + newOrientedEdge.alias.aliases.push_back(number); + newOrientedEdge.edgeCurveId = edgeCurveId; + newOrientedEdge.flip = flip; + + orientedEdgeAliases.push_back(newOrientedEdge); + return false; +} + +// Return an oriented edge index. As above. +int StepFileWriter::InsertOrientedEdge(int number) { + for(orientedEdgeAliases_t e : orientedEdgeAliases) { + for(int alias : e.alias.aliases) { + if(alias == number) { + return e.alias.reference; + } + } + } + + // ERROR: it should never reach this point... + return -1; +} + +//----------------------------------------------------------------------------- +// Functions for STEP export: print to file. +//----------------------------------------------------------------------------- +void StepFileWriter::WriteHeader() { + fprintf(f, +"ISO-10303-21;\n" +"HEADER;\n" +"\n" +"FILE_DESCRIPTION((''), '2;1');\n" +"\n" +"FILE_NAME(\n" +" 'output_file',\n" +" '2009-06-07T17:44:47-07:00',\n" +" (''),\n" +" (''),\n" +" 'SolveSpace',\n" +" '',\n" +" ''\n" +");\n" +"\n" +"FILE_SCHEMA (('CONFIG_CONTROL_DESIGN'));\n" +"ENDSEC;\n" +"\n" +"DATA;\n" +"\n" +"/**********************************************************\n" +" * This defines the units and tolerances for the file. It\n" +" * is always the same, independent of the actual data.\n" +" **********************************************************/\n" +"#158=(\n" +"LENGTH_UNIT()\n" +"NAMED_UNIT(*)\n" +"SI_UNIT(.MILLI.,.METRE.)\n" +");\n" +"#161=(\n" +"NAMED_UNIT(*)\n" +"PLANE_ANGLE_UNIT()\n" +"SI_UNIT($,.RADIAN.)\n" +");\n" +"#166=(\n" +"NAMED_UNIT(*)\n" +"SI_UNIT($,.STERADIAN.)\n" +"SOLID_ANGLE_UNIT()\n" +");\n" +"#167=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(%f),#158,\n" +"'DISTANCE_ACCURACY_VALUE',\n" +"'string');\n" +"#168=(\n" +"GEOMETRIC_REPRESENTATION_CONTEXT(3)\n" +"GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#167))\n" +"GLOBAL_UNIT_ASSIGNED_CONTEXT((#166,#161,#158))\n" +"REPRESENTATION_CONTEXT('ID1','3D')\n" +");\n" +"#169=SHAPE_REPRESENTATION('',(#170),#168);\n" +"#170=AXIS2_PLACEMENT_3D('',#173,#171,#172);\n" +"#171=DIRECTION('',(0.,0.,1.));\n" +"#172=DIRECTION('',(1.,0.,0.));\n" +"#173=CARTESIAN_POINT('',(0.,0.,0.));\n" +"\n", + PRECISION); + + // Start the ID somewhere beyond the header IDs. + id = 200; +} +void StepFileWriter::WriteProductHeader() { + fprintf(f, + "#175 = SHAPE_DEFINITION_REPRESENTATION(#176, #169);\n" + "#176 = PRODUCT_DEFINITION_SHAPE('Version', 'Test Part', #177);\n" + "#177 = PRODUCT_DEFINITION('Version', 'Test Part', #182, #178);\n" + "#178 = DESIGN_CONTEXT('3D Mechanical Parts', #181, 'design');\n" + "#179 = PRODUCT('1', 'Product', 'Test Part', (#180));\n" + "#180 = MECHANICAL_CONTEXT('3D Mechanical Parts', #181, 'mechanical');\n" + "#181 = APPLICATION_CONTEXT(\n" + "'configuration controlled 3d designs of mechanical parts and assemblies');\n" + "#182 = PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE('Version',\n" + "'Test Part', #179, .MADE.);\n" + "\n" + ); +} +int StepFileWriter::ExportCurve(SBezier *sb) { + int i, ret = id; + std::vector curvePoints = {}; + + for(i = 0; i <= sb->deg; i++) { + if (!HasCartesianPointAnAlias(id + 1 + i, sb->ctrl[i], -1)) + fprintf(f, CARTESIAN_POINT_FORMAT, + id + 1 + i, + CO(sb->ctrl[i])); + } + + for(i = 0; i <= sb->deg; i++) { + int point = InsertPoint(id + 1 + i); + curvePoints.push_back(point); + } + + if (!HasBSplineCurveAnAlias(ret, curvePoints)) { + fprintf(f, "#%d=(\n", ret); + fprintf(f, "BOUNDED_CURVE()\n"); + fprintf(f, "B_SPLINE_CURVE(%d,(", sb->deg); + for(i = 0; i <= sb->deg; i++) { + fprintf(f, "#%d", InsertPoint(ret + i + 1)); + if(i != sb->deg) fprintf(f, ","); + } + fprintf(f, "),.UNSPECIFIED.,.F.,.F.)\n"); + fprintf(f, "B_SPLINE_CURVE_WITH_KNOTS((%d,%d),", + (sb->deg + 1), (sb-> deg + 1)); + fprintf(f, "(0.000,1.000),.UNSPECIFIED.)\n"); + fprintf(f, "CURVE()\n"); + fprintf(f, "GEOMETRIC_REPRESENTATION_ITEM()\n"); + fprintf(f, "RATIONAL_B_SPLINE_CURVE(("); + for(i = 0; i <= sb->deg; i++) { + fprintf(f, "%.10f", sb->weight[i]); + if(i != sb->deg) fprintf(f, ","); + } + fprintf(f, "))\n"); + fprintf(f, "REPRESENTATION_ITEM('')\n);\n"); + fprintf(f, "\n"); + } + + id = ret + 1 + (sb->deg + 1); + return ret; +} + +int StepFileWriter::ExportCurveLoop(SBezierLoop *loop, bool inner) { + ssassert(loop->l.n >= 1, "Expected at least one loop"); + + List listOfTrims = {}; + + SBezier *sb = loop->l.Last(); + + int lastFinish, prevFinish; + // Generate "exactly closed" contours, with the same vertex id for the + // finish of a previous edge and the start of the next one. So we need + // the finish of the last Bezier in the loop before we start our process. + bool vertex_has_alias; + if(!HasCartesianPointAnAlias(id, sb->Finish(), id + 1, &vertex_has_alias)) { + fprintf(f, CARTESIAN_POINT_FORMAT, + id, CO(sb->Finish())); + fprintf(f, "#%d=VERTEX_POINT('',#%d);\n", id + 1, InsertPoint(id)); + lastFinish = id + 1; + } else if(!vertex_has_alias) { + fprintf(f, "#%d=VERTEX_POINT('',#%d);\n", id + 1, InsertPoint(id)); + lastFinish = id + 1; + } + else { + lastFinish = InsertVertex(id+1); + } + prevFinish = lastFinish; + id += 2; + + for(sb = loop->l.First(); sb; sb = loop->l.NextAfter(sb)) { + int curveId = ExportCurve(sb); + + int thisFinish; + if(loop->l.NextAfter(sb) != NULL) { + if(!HasCartesianPointAnAlias(id, sb->Finish(), id + 1, &vertex_has_alias)) { + fprintf(f, CARTESIAN_POINT_FORMAT, + id, CO(sb->Finish())); + fprintf(f, "#%d=VERTEX_POINT('',#%d);\n", id + 1, InsertPoint(id)); + thisFinish = id + 1; + } else if(!vertex_has_alias) { + fprintf(f, "#%d=VERTEX_POINT('',#%d);\n", id + 1, InsertPoint(id)); + thisFinish = id + 1; + } else { + thisFinish = InsertVertex(id+1); + } + id += 2; + } else { + thisFinish = lastFinish; + } + + bool flip_edge; + if (!HasEdgeCurveAnAlias(id, prevFinish, thisFinish, InsertCurve(curveId), &flip_edge)) { + fprintf(f, "#%d=EDGE_CURVE('',#%d,#%d,#%d,%s);\n", + id, prevFinish, thisFinish, InsertCurve(curveId), ".T."); + if(!HasOrientedEdgeAnAlias(id + 1, id, flip_edge)) { + fprintf(f, "#%d=ORIENTED_EDGE('',*,*,#%d,.T.);\n", id + 1, id); + } else { + ssassert(false, "Impossible"); + } + } else if(!HasOrientedEdgeAnAlias(id + 1, id, flip_edge)) { + fprintf(f, "#%d=ORIENTED_EDGE('',*,*,#%d,.%c.);\n", id + 1, InsertEdgeCurve(id), + flip_edge ? 'F' : 'T'); + } + + int oe = id+1; + listOfTrims.Add(&oe); + id += 2; + + prevFinish = thisFinish; + } + + fprintf(f, "#%d=EDGE_LOOP('',(", id); + int *oe; + for(oe = listOfTrims.First(); oe; oe = listOfTrims.NextAfter(oe)) { + fprintf(f, "#%d", InsertOrientedEdge(*oe)); + if(listOfTrims.NextAfter(oe) != NULL) fprintf(f, ","); + } + fprintf(f, "));\n"); + + int fb = id + 1; + fprintf(f, "#%d=%s('',#%d,.T.);\n", + fb, inner ? "FACE_BOUND" : "FACE_OUTER_BOUND", id); + + id += 2; + listOfTrims.Clear(); + + return fb; +} + +void StepFileWriter::ExportSurface(SSurface *ss, SBezierList *sbl) { + int i, j, srfid = id; + + // Read the colour of the surface: use it to tell apart surfaces + // from different parts. + currentColor = ss->color; + + // First, define the control points for the untrimmed surface, if they + // were not already defined. + for(i = 0; i <= ss->degm; i++) { + for(j = 0; j <= ss->degn; j++) { + if (!HasCartesianPointAnAlias(srfid + 1 + j + i*(ss->degn + 1), + ss->ctrl[i][j], -1)) { + fprintf(f, CARTESIAN_POINT_FORMAT, + srfid + 1 + j + i*(ss->degn + 1), + CO(ss->ctrl[i][j])); + } + } + } + + // Then, we create the untrimmed surface. We always specify a rational + // B-spline surface (in fact, just a Bezier surface). + fprintf(f, "#%d=(\n", srfid); + fprintf(f, "BOUNDED_SURFACE()\n"); + fprintf(f, "B_SPLINE_SURFACE(%d,%d,(", ss->degm, ss->degn); + for(i = 0; i <= ss->degm; i++) { + fprintf(f, "("); + for(j = 0; j <= ss->degn; j++) { + fprintf(f, "#%d", InsertPoint(srfid + 1 + j + i*(ss->degn + 1))); + if(j != ss->degn) fprintf(f, ","); + } + fprintf(f, ")"); + if(i != ss->degm) fprintf(f, ","); + } + fprintf(f, "),.UNSPECIFIED.,.F.,.F.,.F.)\n"); + fprintf(f, "B_SPLINE_SURFACE_WITH_KNOTS((%d,%d),(%d,%d),", + (ss->degm + 1), (ss->degm + 1), + (ss->degn + 1), (ss->degn + 1)); + fprintf(f, "(0.000,1.000),(0.000,1.000),.UNSPECIFIED.)\n"); + fprintf(f, "GEOMETRIC_REPRESENTATION_ITEM()\n"); + fprintf(f, "RATIONAL_B_SPLINE_SURFACE(("); + for(i = 0; i <= ss->degm; i++) { + fprintf(f, "("); + for(j = 0; j <= ss->degn; j++) { + fprintf(f, "%.10f", ss->weight[i][j]); + if(j != ss->degn) fprintf(f, ","); + } + fprintf(f, ")"); + if(i != ss->degm) fprintf(f, ","); + } + fprintf(f, "))\n"); + fprintf(f, "REPRESENTATION_ITEM('')\n"); + fprintf(f, "SURFACE()\n"); + fprintf(f, ");\n"); + + fprintf(f, "\n"); + + id = srfid + 1 + (ss->degm + 1)*(ss->degn + 1); + + // Now we do the trim curves. We must group each outer loop separately + // along with its inner faces, so do that now. + SBezierLoopSetSet sblss = {}; + SPolygon spxyz = {}; + bool allClosed; + SEdge notClosedAt; + // We specify a surface, so it doesn't check for coplanarity; and we + // don't want it to give us any open contours. The polygon and chord + // tolerance are required, because they are used to calculate the + // contour directions and determine inner vs. outer contours. + sblss.FindOuterFacesFrom(sbl, &spxyz, ss, + SS.ExportChordTolMm(), + &allClosed, ¬ClosedAt, + NULL, NULL, + NULL); + + // So in our list of SBezierLoopSet, each set contains at least one loop + // (the outer boundary), plus any inner loops associated with that outer + // loop. + SBezierLoopSet *sbls; + for(sbls = sblss.l.First(); sbls; sbls = sblss.l.NextAfter(sbls)) { + SBezierLoop *loop = sbls->l.First(); + + List listOfLoops = {}; + // Create the face outer boundary from the outer loop. + int fob = ExportCurveLoop(loop, /*inner=*/false); + listOfLoops.Add(&fob); + + // And create the face inner boundaries from any inner loops that + // lie within this contour. + loop = sbls->l.NextAfter(loop); + for(; loop; loop = sbls->l.NextAfter(loop)) { + int fib = ExportCurveLoop(loop, /*inner=*/true); + listOfLoops.Add(&fib); + } + + // And now create the face that corresponds to this outer loop + // and all of its holes. + int advFaceId = id; + fprintf(f, "#%d=ADVANCED_FACE('',(", advFaceId); + int *fb; + for(fb = listOfLoops.First(); fb; fb = listOfLoops.NextAfter(fb)) { + fprintf(f, "#%d", *fb); + if(listOfLoops.NextAfter(fb) != NULL) fprintf(f, ","); + } + + fprintf(f, "),#%d,.T.);\n", srfid); + advancedFaces.Add(&advFaceId); + + // Export the surface color and transparency + // https://www.cax-if.org/documents/rec_prac_styling_org_v16.pdf sections 4.4.2 4.2.4 etc. + // https://tracker.dev.opencascade.org/view.php?id=31550 + fprintf(f, "#%d=COLOUR_RGB('',%.2f,%.2f,%.2f);\n", ++id, ss->color.redF(), + ss->color.greenF(), ss->color.blueF()); + +/* // This works in Kisters 3DViewStation but not in KiCAD and Horison EDA, + // it seems they do not support transparency so use the more verbose one below + fprintf(f, "#%d=SURFACE_STYLE_TRANSPARENT(%.2f);\n", ++id, 1.0 - ss->color.alphaF()); + ++id; + fprintf(f, "#%d=SURFACE_STYLE_RENDERING_WITH_PROPERTIES(.NORMAL_SHADING.,#%d,(#%d));\n", + id, id - 2, id - 1); + ++id; + fprintf(f, "#%d=SURFACE_SIDE_STYLE('',(#%d));\n", id, id - 1); +*/ + + // This works in Horison EDA but is more verbose. + ++id; + fprintf(f, "#%d=FILL_AREA_STYLE_COLOUR('',#%d);\n", id, id - 1); + ++id; + fprintf(f, "#%d=FILL_AREA_STYLE('',(#%d));\n", id, id - 1); + ++id; + fprintf(f, "#%d=SURFACE_STYLE_FILL_AREA(#%d);\n", id, id - 1); + fprintf(f, "#%d=SURFACE_STYLE_TRANSPARENT(%.2f);\n", ++id, 1.0 - ss->color.alphaF()); + ++id; + fprintf(f, "#%d=SURFACE_STYLE_RENDERING_WITH_PROPERTIES(.NORMAL_SHADING.,#%d,(#%d));\n", id, id - 5, id - 1); + ++id; + fprintf(f, "#%d=SURFACE_SIDE_STYLE('',(#%d, #%d));\n", id, id - 3, id - 1); + + ++id; + fprintf(f, "#%d=SURFACE_STYLE_USAGE(.BOTH.,#%d);\n", id, id - 1); + ++id; + fprintf(f, "#%d=PRESENTATION_STYLE_ASSIGNMENT((#%d));\n", id, id - 1); + ++id; + fprintf(f, "#%d=STYLED_ITEM('',(#%d),#%d);\n", id, id - 1, advFaceId); + fprintf(f, "\n"); + + id++; + listOfLoops.Clear(); + } + sblss.Clear(); + spxyz.Clear(); +} + +void StepFileWriter::WriteFooter() { + fprintf(f, +"\n" +"ENDSEC;\n" +"\n" +"END-ISO-10303-21;\n" + ); +} + +void StepFileWriter::ExportSurfacesTo(const Platform::Path &filename) { + Group *g = SK.GetGroup(SS.GW.activeGroup); + SShell *shell = &(g->runningShell); + + if(shell->surface.IsEmpty()) { + Error("The model does not contain any surfaces to export.%s", + !g->runningMesh.l.IsEmpty() + ? "\n\nThe model does contain triangles from a mesh, but " + "a triangle mesh cannot be exported as a STEP file. Try " + "File -> Export Mesh... instead." + : ""); + return; + } + + f = OpenFile(filename, "wb"); + if(!f) { + Error("Couldn't write to '%s'", filename.raw.c_str()); + return; + } + + // Initialization of lists. + pointAliases = {}; + curveAliases = {}; + edgeCurveAliases = {}; + orientedEdgeAliases = {}; + + WriteHeader(); + WriteProductHeader(); + + advancedFaces = {}; + + for(SSurface &ss : shell->surface) { + if(ss.trim.IsEmpty()) + continue; + + // Get all of the loops of Beziers that trim our surface (with each + // Bezier split so that we use the section as t goes from 0 to 1), and + // the piecewise linearization of those loops in xyz space. + SBezierList sbl = {}; + ss.MakeSectionEdgesInto(shell, NULL, &sbl); + + // Apply the export scale factor. + ss.ScaleSelfBy(1.0/SS.exportScale); + sbl.ScaleSelfBy(1.0/SS.exportScale); + + ExportSurface(&ss, &sbl); + + sbl.Clear(); + } + + fprintf(f, "#%d=CLOSED_SHELL('',(", id); + int *af; + for(af = advancedFaces.First(); af; af = advancedFaces.NextAfter(af)) { + fprintf(f, "#%d", *af); + if(advancedFaces.NextAfter(af) != NULL) fprintf(f, ","); + } + fprintf(f, "));\n"); + fprintf(f, "#%d=MANIFOLD_SOLID_BREP('brep',#%d);\n", id+1, id); + fprintf(f, "#%d=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#%d,#170),#168);\n", + id+2, id+1); + fprintf(f, "#%d=SHAPE_REPRESENTATION_RELATIONSHIP($,$,#169,#%d);\n", + id+3, id+2); + + WriteFooter(); + + fclose(f); + advancedFaces.Clear(); + pointAliases.clear(); + curveAliases.clear(); + edgeCurveAliases.clear(); + orientedEdgeAliases.clear(); +} + +void StepFileWriter::WriteWireframe() { + fprintf(f, "#%d=GEOMETRIC_CURVE_SET('curves',(", id); + int *c; + for(c = curves.First(); c; c = curves.NextAfter(c)) { + fprintf(f, "#%d", *c); + if(curves.NextAfter(c) != NULL) fprintf(f, ","); + } + fprintf(f, "));\n"); + fprintf(f, "#%d=GEOMETRICALLY_BOUNDED_WIREFRAME_SHAPE_REPRESENTATION" + "('',(#%d,#170),#168);\n", id+1, id); + fprintf(f, "#%d=SHAPE_REPRESENTATION_RELATIONSHIP($,$,#169,#%d);\n", + id+2, id+1); + + id += 3; + curves.Clear(); +} + +} // namespace SolveSpace diff --git a/src/exportvector.cpp b/src/exportvector.cpp new file mode 100644 index 0000000..1753997 --- /dev/null +++ b/src/exportvector.cpp @@ -0,0 +1,1360 @@ +//----------------------------------------------------------------------------- +// The file format-specific stuff for all of the 2d vector output formats. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include +#include "solvespace.h" + +namespace SolveSpace { + +//----------------------------------------------------------------------------- +// Routines for DXF export +//----------------------------------------------------------------------------- +class DxfWriteInterface : public DRW_Interface { +public: + DxfFileWriter *writer; + dxfRW *dxf; + + std::set messages; + + static DRW_Coord toCoord(const Vector &v) { + return DRW_Coord(v.x, v.y, v.z); + } + + Vector xfrm(Vector v) { + return writer->Transform(v); + } + + void writeTextstyles() override { + DRW_Textstyle ts; + ts.name = "unicode"; + ts.font = "unicode"; + dxf->writeTextstyle(&ts); + } + + void writeLayers() override { + DRW_Layer layer; + + layer.name = "dimensions"; + dxf->writeLayer(&layer); + layer.name = "text"; + dxf->writeLayer(&layer); + + std::set usedStyles; + + for(DxfFileWriter::BezierPath &path : writer->paths) { + for(SBezier *sb : path.beziers) { + usedStyles.insert((uint32_t)sb->auxA); + } + } + + for(uint32_t v : usedStyles) { + Style *s = Style::Get(hStyle{v}); + layer.name = s->DescriptionString(); + dxf->writeLayer(&layer); + } + } + + void writeLTypes() override { + for(uint32_t i = 0; i <= (uint32_t)StipplePattern::LAST; i++) { + StipplePattern st = (StipplePattern)i; + DRW_LType type; + // LibreCAD requires the line type to have one of these exact names, + // or otherwise it overwrites it with its own (continuous) style. + type.name = DxfFileWriter::lineTypeName(st); + double sw = 1.0; + switch(st) { + case StipplePattern::CONTINUOUS: + break; + + case StipplePattern::SHORT_DASH: + type.path.push_back(sw); + type.path.push_back(-sw * 2.0); + break; + + case StipplePattern::DASH: + type.path.push_back(sw); + type.path.push_back(-sw); + break; + + case StipplePattern::LONG_DASH: + type.path.push_back(sw * 2.0); + type.path.push_back(-sw); + break; + + case StipplePattern::DASH_DOT: + type.path.push_back(sw); + type.path.push_back(-sw); + type.path.push_back(0.0); + type.path.push_back(-sw); + break; + + case StipplePattern::DOT: + type.path.push_back(sw); + type.path.push_back(0.0); + break; + + case StipplePattern::DASH_DOT_DOT: + type.path.push_back(sw); + type.path.push_back(-sw); + type.path.push_back(0.0); + type.path.push_back(-sw); + type.path.push_back(0.0); + type.path.push_back(-sw); + break; + + case StipplePattern::FREEHAND: + case StipplePattern::ZIGZAG: + // Not implemented; exported as continuous. + break; + } + dxf->writeLineType(&type); + } + } + + void writePolylines() { + PolylineBuilder builder; + + for(DxfFileWriter::BezierPath &path : writer->paths) { + for(SBezier *sb : path.beziers) { + if(sb->deg != 1) continue; + builder.AddEdge(sb->ctrl[0], sb->ctrl[1], (uint32_t)sb->auxA); + } + } + + DRW_Polyline polyline; + + auto startFunc = [&](PolylineBuilder::Vertex *start, + PolylineBuilder::Vertex *next, + PolylineBuilder::Edge *e) { + hStyle hs = { e->kind }; + polyline = {}; + assignEntityDefaults(&polyline, hs); + + if(!(EXACT(start->pos.z == 0.0) && EXACT(next->pos.z == 0.0))) { + polyline.flags |= 8 /* 3d polyline */; + } + polyline.vertlist.push_back( + std::make_shared(start->pos.x, start->pos.y, start->pos.z, 0.0)); + polyline.vertlist.push_back( + std::make_shared(next->pos.x, next->pos.y, next->pos.z, 0.0)); + }; + + auto nextFunc = [&](PolylineBuilder::Vertex *next, PolylineBuilder::Edge *e) { + if(!EXACT(next->pos.z == 0.0)) { + polyline.flags |= 8 /* 3d polyline */; + } + polyline.vertlist.push_back( + std::make_shared(next->pos.x, next->pos.y, next->pos.z, 0.0)); + }; + + auto endFunc = [&]() { + dxf->writePolyline(&polyline); + }; + + auto aloneFunc = [&](PolylineBuilder::Edge *e) { + hStyle hs = { e->kind }; + writeLine(e->a->pos, e->b->pos, hs); + }; + + builder.Generate(startFunc, nextFunc, aloneFunc, endFunc); + } + + void writeEntities() override { + writePolylines(); + + for(DxfFileWriter::BezierPath &path : writer->paths) { + for(SBezier *sb : path.beziers) { + if(sb->deg == 1) continue; + writeBezier(sb); + } + } + + if(writer->constraint) { + for(Constraint &c : *writer->constraint) { + if(!writer->NeedToOutput(&c)) continue; + switch(c.type) { + case Constraint::Type::PT_PT_DISTANCE: { + Vector ap = SK.GetEntity(c.ptA)->PointGetNum(); + Vector bp = SK.GetEntity(c.ptB)->PointGetNum(); + Vector ref = ((ap.Plus(bp)).ScaledBy(0.5)).Plus(c.disp.offset); + writeAlignedDimension(xfrm(ap), xfrm(bp), xfrm(ref), + xfrm(ref), c.Label(), c.GetStyle(), c.valA); + break; + } + + case Constraint::Type::PT_LINE_DISTANCE: { + Vector pt = SK.GetEntity(c.ptA)->PointGetNum(); + Entity *line = SK.GetEntity(c.entityA); + Vector lA = SK.GetEntity(line->point[0])->PointGetNum(); + Vector lB = SK.GetEntity(line->point[1])->PointGetNum(); + Vector dl = lB.Minus(lA); + + Vector closest = pt.ClosestPointOnLine(lA, dl); + + if(pt.Equals(closest)) break; + + Vector ref = ((closest.Plus(pt)).ScaledBy(0.5)).Plus(c.disp.offset); + Vector refClosest = ref.ClosestPointOnLine(lA, dl); + + double ddl = dl.Dot(dl); + if(fabs(ddl) > LENGTH_EPS * LENGTH_EPS) { + double t = refClosest.Minus(lA).Dot(dl) / ddl; + if(t < 0.0) { + refClosest = lA; + } else if(t > 1.0) { + refClosest = lB; + } + } + + Vector xdl = xfrm(lB).Minus(xfrm(lA)); + writeLinearDimension(xfrm(pt), xfrm(refClosest), xfrm(ref), + xfrm(ref), c.Label(), + atan2(xdl.y, xdl.x) / PI * 180.0 + 90.0, 0.0, + c.GetStyle(), c.valA); + break; + } + + case Constraint::Type::DIAMETER: { + Entity *circle = SK.GetEntity(c.entityA); + Vector center = SK.GetEntity(circle->point[0])->PointGetNum(); + Quaternion q = SK.GetEntity(circle->normal)->NormalGetNum(); + Vector n = q.RotationN().WithMagnitude(1); + double r = circle->CircleGetRadiusNum(); + + Vector ref = center.Plus(c.disp.offset); + // Force the label into the same plane as the circle. + ref = ref.Minus(n.ScaledBy(n.Dot(ref) - n.Dot(center))); + + Vector rad = ref.Minus(center).WithMagnitude(r); + if(/*isRadius*/c.other) { + writeRadialDimension( + xfrm(center), xfrm(center.Plus(rad)), + xfrm(ref), c.Label(), c.GetStyle(), c.valA); + } else { + writeDiametricDimension( + xfrm(center.Minus(rad)), xfrm(center.Plus(rad)), + xfrm(ref), c.Label(), c.GetStyle(), c.valA); + } + break; + } + + case Constraint::Type::ANGLE: { + Entity *a = SK.GetEntity(c.entityA); + Entity *b = SK.GetEntity(c.entityB); + + Vector a0 = a->VectorGetStartPoint(); + Vector b0 = b->VectorGetStartPoint(); + Vector da = a->VectorGetNum(); + Vector db = b->VectorGetNum(); + if(/*otherAngle*/c.other) { + a0 = a0.Plus(da); + da = da.ScaledBy(-1); + } + + bool skew = false; + Vector ref = c.disp.offset; + Vector pi = Vector::AtIntersectionOfLines(a0, a0.Plus(da), b0, b0.Plus(db), + &skew); + if(!skew) ref = pi.Plus(c.disp.offset); + + Vector norm = da.Cross(db); + Vector dna = norm.Cross(da).WithMagnitude(1.0); + + double thetaf = acos(da.DirectionCosineWith(db)); + + // Calculate median + Vector m = da.WithMagnitude(1.0).ScaledBy(cos(thetaf/2)).Plus( + dna.ScaledBy(sin(thetaf/2))); + Vector rm = ref.Minus(pi); + + // Test which side we have to place an arc + if(m.Dot(rm) < 0) { + da = da.ScaledBy(-1); dna = dna.ScaledBy(-1); + db = db.ScaledBy(-1); + } + + Vector bisect = da.WithMagnitude(1.0).ScaledBy(cos(thetaf / 2.0)).Plus( + dna.ScaledBy(sin(thetaf / 2.0))); + + ref = pi.Plus(bisect.WithMagnitude(c.disp.offset.Magnitude())); + + // Get lines again to write exact line. + a0 = a->VectorGetStartPoint(); + b0 = b->VectorGetStartPoint(); + da = a->VectorGetNum(); + db = b->VectorGetNum(); + + writeAngularDimension( + xfrm(a0), xfrm(a0.Plus(da)), xfrm(b0), xfrm(b0.Plus(db)), xfrm(ref), + xfrm(ref), c.Label(), c.GetStyle(), c.valA); + break; + } + + case Constraint::Type::COMMENT: { + Style *st = SK.style.FindById(c.GetStyle()); + writeText(xfrm(c.disp.offset), c.Label(), + Style::TextHeight(c.GetStyle()) / SS.GW.scale, + st->textAngle, st->textOrigin, c.GetStyle()); + break; + } + + default: + // Other types of constraints do not have a DXF dimension equivalent. + break; + } + } + } + } + + int findDxfColor(const RgbaColor &src) { + int best = 0; + double minDist = VERY_POSITIVE; + Vector srcv = Vector::From(src.redF(), src.greenF(), src.blueF()); + for(int i = 1; i < 256; i++) { + RgbaColor dst = RGBi(DRW::dxfColors[i][0], DRW::dxfColors[i][1], DRW::dxfColors[i][2]); + Vector dstv = Vector::From(dst.redF(), dst.greenF(), dst.blueF()); + double dist = srcv.Minus(dstv).Magnitude(); + if(dist < minDist || best == 0) { + best = i; + minDist = dist; + } + } + return best; + } + + void assignEntityDefaults(DRW_Entity *entity, hStyle hs) { + Style *s = Style::Get(hs); + RgbaColor color = Style::Color(hs, /*forExport=*/true); + entity->color24 = color.ToPackedIntBGRA(); + entity->color = findDxfColor(color); + entity->layer = s->DescriptionString(); + entity->lineType = DxfFileWriter::lineTypeName(s->stippleType); + entity->ltypeScale = Style::StippleScaleMm(s->h); + entity->setWidthMm(Style::WidthMm(hs.v)); + + if(s->stippleType == StipplePattern::FREEHAND) { + messages.insert(_("freehand lines were replaced with continuous lines")); + } else if(s->stippleType == StipplePattern::ZIGZAG) { + messages.insert(_("zigzag lines were replaced with continuous lines")); + } + } + + void assignDimensionDefaults(DRW_Dimension *dimension, hStyle hs) { + assignEntityDefaults(dimension, hs); + dimension->layer = "dimensions"; + } + + void writeLine(const Vector &p0, const Vector &p1, hStyle hs) { + DRW_Line line; + assignEntityDefaults(&line, hs); + line.basePoint = toCoord(p0); + line.secPoint = toCoord(p1); + dxf->writeLine(&line); + } + + void writeArc(const Vector &c, double r, double sa, double ea, hStyle hs) { + DRW_Arc arc; + assignEntityDefaults(&arc, hs); + arc.radious = r; + arc.basePoint = toCoord(c); + arc.staangle = sa; + arc.endangle = ea; + dxf->writeArc(&arc); + } + + void writeBezierAsPwl(SBezier *sb) { + List lv = {}; + sb->MakePwlInto(&lv, SS.ExportChordTolMm()); + hStyle hs = { (uint32_t)sb->auxA }; + DRW_Polyline polyline; + assignEntityDefaults(&polyline, hs); + for(int i = 0; i < lv.n; i++) { + Vector *v = &lv[i]; + polyline.vertlist.push_back(std::make_shared(v->x, v->y, v->z, 0.0)); + } + dxf->writePolyline(&polyline); + lv.Clear(); + } + + void makeKnotsFor(DRW_Spline *spline) { + // QCad/LibreCAD require this for some reason. + if(spline->degree == 3) { + spline->nknots = 8; + spline->knotslist.push_back(0.0); + spline->knotslist.push_back(0.0); + spline->knotslist.push_back(0.0); + spline->knotslist.push_back(0.0); + spline->knotslist.push_back(1.0); + spline->knotslist.push_back(1.0); + spline->knotslist.push_back(1.0); + spline->knotslist.push_back(1.0); + } else if(spline->degree == 2) { + spline->nknots = 6; + spline->knotslist.push_back(0.0); + spline->knotslist.push_back(0.0); + spline->knotslist.push_back(0.0); + spline->knotslist.push_back(1.0); + spline->knotslist.push_back(1.0); + spline->knotslist.push_back(1.0); + } else ssassert(false, "Unexpected degree of spline"); + } + + void writeSpline(SBezier *sb) { + bool isRational = sb->IsRational(); + hStyle hs = { (uint32_t)sb->auxA }; + DRW_Spline spline; + assignEntityDefaults(&spline, hs); + spline.flags = (isRational) ? 0x04 : 0x08; + spline.degree = sb->deg; + spline.ncontrol = sb->deg + 1; + makeKnotsFor(&spline); + for(int i = 0; i <= sb->deg; i++) { + spline.controllist.push_back( + std::make_shared(sb->ctrl[i].x, sb->ctrl[i].y, sb->ctrl[i].z)); + if(isRational) spline.weightlist.push_back(sb->weight[i]); + } + dxf->writeSpline(&spline); + } + + void writeBezier(SBezier *sb) { + hStyle hs = { (uint32_t)sb->auxA }; + Vector c; + Vector n = Vector::From(0.0, 0.0, 1.0); + double r; + + if(sb->deg == 1) { + // Line + writeLine(sb->ctrl[0], sb->ctrl[1], hs); + } else if(sb->IsInPlane(n, 0) && sb->IsCircle(n, &c, &r)) { + // Circle perpendicular to camera + double theta0 = atan2(sb->ctrl[0].y - c.y, sb->ctrl[0].x - c.x); + double theta1 = atan2(sb->ctrl[2].y - c.y, sb->ctrl[2].x - c.x); + double dtheta = WRAP_SYMMETRIC(theta1 - theta0, 2.0 * PI); + if(dtheta < 0.0) swap(theta0, theta1); + + writeArc(c, r, theta0, theta1, hs); + } else if(sb->IsRational()) { + // Rational bezier + // We'd like to export rational beziers exactly, but the resulting DXF + // files can only be read by AutoCAD; LibreCAD/QCad simply do not + // implement the feature. So, export as piecewise linear for compatibility. + writeBezierAsPwl(sb); + } else { + // Any other curve + writeSpline(sb); + } + } + + void writeAlignedDimension(Vector def1, Vector def2, Vector dimp, + Vector textp, const std::string &text, hStyle hs, double actual) { + DRW_DimAligned dim; + assignDimensionDefaults(&dim, hs); + dim.setDef1Point(toCoord(def1)); + dim.setDef2Point(toCoord(def2)); + dim.setDimPoint(toCoord(dimp)); + dim.setTextPoint(toCoord(textp)); + dim.setText(text); + dim.setActualMeasurement(actual); + dxf->writeDimension(&dim); + } + + void writeLinearDimension(Vector def1, Vector def2, Vector dimp, + Vector textp, const std::string &text, + double angle, double oblique, hStyle hs, double actual) { + DRW_DimLinear dim; + assignDimensionDefaults(&dim, hs); + dim.setDef1Point(toCoord(def1)); + dim.setDef2Point(toCoord(def2)); + dim.setDimPoint(toCoord(dimp)); + dim.setTextPoint(toCoord(textp)); + dim.setText(text); + dim.setAngle(angle); + dim.setOblique(oblique); + dim.setActualMeasurement(actual); + dxf->writeDimension(&dim); + } + + void writeRadialDimension(Vector center, Vector radius, + Vector textp, const std::string &text, hStyle hs, double actual) { + DRW_DimRadial dim; + assignDimensionDefaults(&dim, hs); + dim.setCenterPoint(toCoord(center)); + dim.setDiameterPoint(toCoord(radius)); + dim.setTextPoint(toCoord(textp)); + dim.setText(text); + dim.setActualMeasurement(actual); + dxf->writeDimension(&dim); + } + + void writeDiametricDimension(Vector def1, Vector def2, + Vector textp, const std::string &text, hStyle hs, double actual) { + DRW_DimDiametric dim; + assignDimensionDefaults(&dim, hs); + dim.setDiameter1Point(toCoord(def1)); + dim.setDiameter2Point(toCoord(def2)); + dim.setTextPoint(toCoord(textp)); + dim.setText(text); + dim.setActualMeasurement(actual); + dxf->writeDimension(&dim); + } + + void writeAngularDimension(Vector fl1, Vector fl2, Vector sl1, Vector sl2, Vector dimp, + Vector textp, const std::string &text, hStyle hs, double actual) { + DRW_DimAngular dim; + assignDimensionDefaults(&dim, hs); + dim.setFirstLine1(toCoord(fl1)); + dim.setFirstLine2(toCoord(fl2)); + dim.setSecondLine1(toCoord(sl1)); + dim.setSecondLine2(toCoord(sl2)); + dim.setDimPoint(toCoord(dimp)); + dim.setTextPoint(toCoord(textp)); + dim.setText(text); + dim.setActualMeasurement(actual * PI / 180.0); + dxf->writeDimension(&dim); + } + + void writeText(Vector textp, const std::string &text, + double height, double angle, Style::TextOrigin origin, hStyle hs) { + DRW_Text txt; + assignEntityDefaults(&txt, hs); + txt.layer = "text"; + txt.style = "unicode"; + txt.basePoint = toCoord(textp); + txt.secPoint = txt.basePoint; + txt.text = text; + txt.height = height; + txt.angle = angle; + txt.alignH = DRW_Text::HCenter; + if((uint32_t)origin & (uint32_t)Style::TextOrigin::LEFT) { + txt.alignH = DRW_Text::HLeft; + } else if((uint32_t)origin & (uint32_t)Style::TextOrigin::RIGHT) { + txt.alignH = DRW_Text::HRight; + } + txt.alignV = DRW_Text::VMiddle; + if((uint32_t)origin & (uint32_t)Style::TextOrigin::TOP) { + txt.alignV = DRW_Text::VTop; + } else if((uint32_t)origin & (uint32_t)Style::TextOrigin::BOT) { + txt.alignV = DRW_Text::VBaseLine; + } + dxf->writeText(&txt); + } +}; + +bool DxfFileWriter::OutputConstraints(IdList *constraint) { + this->constraint = constraint; + return true; +} + +void DxfFileWriter::StartFile() { + paths.clear(); +} + +void DxfFileWriter::Background(RgbaColor color) { +} + +void DxfFileWriter::StartPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) +{ + BezierPath path = {}; + paths.push_back(path); +} +void DxfFileWriter::FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) +{ +} + +void DxfFileWriter::Triangle(STriangle *tr) { +} + +void DxfFileWriter::Bezier(SBezier *sb) { + paths.back().beziers.push_back(sb); +} + +void DxfFileWriter::FinishAndCloseFile() { + dxfRW dxf; + + DxfWriteInterface interface = {}; + interface.writer = this; + interface.dxf = &dxf; + + std::stringstream stream; + dxf.write(stream, &interface, DRW::AC1021, /*bin=*/false); + paths.clear(); + constraint = NULL; + + if(!WriteFile(filename, stream.str())) { + Error("Couldn't write to '%s'", filename.raw.c_str()); + return; + } + + if(!interface.messages.empty()) { + std::string text = _("Some aspects of the drawing have no DXF equivalent and " + "were not exported:\n"); + for(const std::string &message : interface.messages) { + text += " * " + message + "\n"; + } + Message(text.c_str()); + } +} + +bool DxfFileWriter::NeedToOutput(Constraint *c) { + switch(c->type) { + case Constraint::Type::PT_PT_DISTANCE: + case Constraint::Type::PT_LINE_DISTANCE: + case Constraint::Type::DIAMETER: + case Constraint::Type::ANGLE: + case Constraint::Type::COMMENT: + return c->IsVisible(); + + default: // See writeEntities(). + break; + } + return false; +} + +const char *DxfFileWriter::lineTypeName(StipplePattern stippleType) { + switch(stippleType) { + case StipplePattern::CONTINUOUS: return "CONTINUOUS"; + case StipplePattern::SHORT_DASH: return "DASHED"; + case StipplePattern::DASH: return "DASHED"; + case StipplePattern::LONG_DASH: return "DASHEDX2"; + case StipplePattern::DASH_DOT: return "DASHDOT"; + case StipplePattern::DASH_DOT_DOT: return "DIVIDE"; + case StipplePattern::DOT: return "DOT"; + + case StipplePattern::FREEHAND: + case StipplePattern::ZIGZAG: + /* no corresponding DXF line type */ + break; + } + + return "CONTINUOUS"; +} + +//----------------------------------------------------------------------------- +// Routines for EPS output +//----------------------------------------------------------------------------- + +static std::string MakeStipplePattern(StipplePattern pattern, double scale, char delimiter, + bool inkscapeWorkaround = false) { + scale /= 2.0; + + // Inkscape ignores all elements that are exactly zero instead of drawing + // them as dots. + double zero = inkscapeWorkaround ? 1e-6 : 0; + + std::string result; + switch(pattern) { + case StipplePattern::CONTINUOUS: + return ""; + + case StipplePattern::SHORT_DASH: + result = ssprintf("%.3f_%.3f", scale, scale * 2.0); + break; + case StipplePattern::DASH: + result = ssprintf("%.3f_%.3f", scale, scale); + break; + case StipplePattern::DASH_DOT: + result = ssprintf("%.3f_%.3f_%.6f_%.3f", + scale, scale * 0.5, zero, scale * 0.5); + break; + case StipplePattern::DASH_DOT_DOT: + result = ssprintf("%.3f_%.3f_%.6f_%.3f_%.6f_%.3f", + scale, scale * 0.5, zero, + scale * 0.5, scale * 0.5, zero); + break; + case StipplePattern::DOT: + result = ssprintf("%.6f_%.3f", zero, scale * 0.5); + break; + case StipplePattern::LONG_DASH: + result = ssprintf("%.3f_%.3f", scale * 2.0, scale * 0.5); + break; + + case StipplePattern::FREEHAND: + case StipplePattern::ZIGZAG: + ssassert(false, "Freehand and zigzag export not implemented"); + } + std::replace(result.begin(), result.end(), '_', delimiter); + return result; +} + +void EpsFileWriter::StartFile() { + fprintf(f, +"%%!PS-Adobe-2.0\r\n" +"%%%%Creator: SolveSpace\r\n" +"%%%%Title: title\r\n" +"%%%%Pages: 0\r\n" +"%%%%PageOrder: Ascend\r\n" +"%%%%BoundingBox: 0 0 %d %d\r\n" +"%%%%HiResBoundingBox: 0 0 %.3f %.3f\r\n" +"%%%%EndComments\r\n" +"\r\n" +"gsave\r\n" +"\r\n", + (int)ceil(MmToPts(ptMax.x - ptMin.x)), + (int)ceil(MmToPts(ptMax.y - ptMin.y)), + MmToPts(ptMax.x - ptMin.x), + MmToPts(ptMax.y - ptMin.y)); +} + +void EpsFileWriter::Background(RgbaColor color) { + double width = ptMax.x - ptMin.x; + double height = ptMax.y - ptMin.y; + + fprintf(f, +"%.3f %.3f %.3f setrgbcolor\r\n" +"newpath\r\n" +" %.3f %.3f moveto\r\n" +" %.3f %.3f lineto\r\n" +" %.3f %.3f lineto\r\n" +" %.3f %.3f lineto\r\n" +" closepath\r\n" +"gsave fill grestore\r\n", + color.redF(), color.greenF(), color.blueF(), + MmToPts(0), MmToPts(0), + MmToPts(width), MmToPts(0), + MmToPts(width), MmToPts(height), + MmToPts(0), MmToPts(height)); + + // same issue with cracks, stroke it to avoid them + double sw = max(width, height) / 1000; + fprintf(f, +"1 setlinejoin\r\n" +"1 setlinecap\r\n" +"%.3f setlinewidth\r\n" +"gsave stroke grestore\r\n", + MmToPts(sw)); +} + +void EpsFileWriter::StartPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) +{ + fprintf(f, "newpath\r\n"); + prevPt = Vector::From(VERY_POSITIVE, VERY_POSITIVE, VERY_POSITIVE); +} +void EpsFileWriter::FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) +{ + StipplePattern pattern = Style::PatternType(hs); + double stippleScale = MmToPts(Style::StippleScaleMm(hs)); + + fprintf(f, " %.3f setlinewidth\r\n" + " %.3f %.3f %.3f setrgbcolor\r\n" + " 1 setlinejoin\r\n" // rounded + " 1 setlinecap\r\n" // rounded + " [%s] 0 setdash\r\n" + " gsave stroke grestore\r\n", + MmToPts(lineWidth), + strokeRgb.redF(), strokeRgb.greenF(), strokeRgb.blueF(), + MakeStipplePattern(pattern, stippleScale, ' ').c_str()); + if(filled) { + fprintf(f, " %.3f %.3f %.3f setrgbcolor\r\n" + " gsave fill grestore\r\n", + fillRgb.redF(), fillRgb.greenF(), fillRgb.blueF()); + } +} + +void EpsFileWriter::MaybeMoveTo(Vector st, Vector fi) { + if(!prevPt.Equals(st)) { + fprintf(f, " %.3f %.3f moveto\r\n", + MmToPts(st.x - ptMin.x), MmToPts(st.y - ptMin.y)); + } + prevPt = fi; +} + +void EpsFileWriter::Triangle(STriangle *tr) { + fprintf(f, +"%.3f %.3f %.3f setrgbcolor\r\n" +"newpath\r\n" +" %.3f %.3f moveto\r\n" +" %.3f %.3f lineto\r\n" +" %.3f %.3f lineto\r\n" +" closepath\r\n" +"gsave fill grestore\r\n", + tr->meta.color.redF(), tr->meta.color.greenF(), tr->meta.color.blueF(), + MmToPts(tr->a.x - ptMin.x), MmToPts(tr->a.y - ptMin.y), + MmToPts(tr->b.x - ptMin.x), MmToPts(tr->b.y - ptMin.y), + MmToPts(tr->c.x - ptMin.x), MmToPts(tr->c.y - ptMin.y)); + + // same issue with cracks, stroke it to avoid them + double sw = max(ptMax.x - ptMin.x, ptMax.y - ptMin.y) / 1000; + fprintf(f, +"1 setlinejoin\r\n" +"1 setlinecap\r\n" +"%.3f setlinewidth\r\n" +"gsave stroke grestore\r\n", + MmToPts(sw)); +} + +void EpsFileWriter::Bezier(SBezier *sb) { + Vector c, n = Vector::From(0, 0, 1); + double r; + if(sb->deg == 1) { + MaybeMoveTo(sb->ctrl[0], sb->ctrl[1]); + fprintf(f, " %.3f %.3f lineto\r\n", + MmToPts(sb->ctrl[1].x - ptMin.x), + MmToPts(sb->ctrl[1].y - ptMin.y)); + } else if(sb->IsCircle(n, &c, &r)) { + Vector p0 = sb->ctrl[0], p1 = sb->ctrl[2]; + double theta0 = atan2(p0.y - c.y, p0.x - c.x), + theta1 = atan2(p1.y - c.y, p1.x - c.x), + dtheta = WRAP_SYMMETRIC(theta1 - theta0, 2*PI); + MaybeMoveTo(p0, p1); + fprintf(f, +" %.3f %.3f %.3f %.3f %.3f %s\r\n", + MmToPts(c.x - ptMin.x), MmToPts(c.y - ptMin.y), + MmToPts(r), + theta0*180/PI, theta1*180/PI, + dtheta < 0 ? "arcn" : "arc"); + } else if(sb->deg == 3 && !sb->IsRational()) { + MaybeMoveTo(sb->ctrl[0], sb->ctrl[3]); + fprintf(f, +" %.3f %.3f %.3f %.3f %.3f %.3f curveto\r\n", + MmToPts(sb->ctrl[1].x - ptMin.x), MmToPts(sb->ctrl[1].y - ptMin.y), + MmToPts(sb->ctrl[2].x - ptMin.x), MmToPts(sb->ctrl[2].y - ptMin.y), + MmToPts(sb->ctrl[3].x - ptMin.x), MmToPts(sb->ctrl[3].y - ptMin.y)); + } else { + BezierAsNonrationalCubic(sb); + } +} + +void EpsFileWriter::FinishAndCloseFile() { + fprintf(f, +"\r\n" +"grestore\r\n" +"\r\n"); + fclose(f); +} + +//----------------------------------------------------------------------------- +// Routines for PDF output, some extra complexity because we have to generate +// a correct xref table. +//----------------------------------------------------------------------------- +void PdfFileWriter::StartFile() { + if((ptMax.x - ptMin.x) > 200*25.4 || + (ptMax.y - ptMin.y) > 200*25.4) + { + Message(_("PDF page size exceeds 200 by 200 inches; many viewers may " + "reject this file.")); + } + + fprintf(f, +"%%PDF-1.1\r\n" +"%%%c%c%c%c\r\n", + 0xe2, 0xe3, 0xcf, 0xd3); + + xref[1] = (uint32_t)ftell(f); + fprintf(f, +"1 0 obj\r\n" +" << /Type /Catalog\r\n" +" /Outlines 2 0 R\r\n" +" /Pages 3 0 R\r\n" +" >>\r\n" +"endobj\r\n"); + + xref[2] = (uint32_t)ftell(f); + fprintf(f, +"2 0 obj\r\n" +" << /Type /Outlines\r\n" +" /Count 0\r\n" +" >>\r\n" +"endobj\r\n"); + + xref[3] = (uint32_t)ftell(f); + fprintf(f, +"3 0 obj\r\n" +" << /Type /Pages\r\n" +" /Kids [4 0 R]\r\n" +" /Count 1\r\n" +" >>\r\n" +"endobj\r\n"); + + xref[4] = (uint32_t)ftell(f); + fprintf(f, +"4 0 obj\r\n" +" << /Type /Page\r\n" +" /Parent 3 0 R\r\n" +" /MediaBox [0 0 %.3f %.3f]\r\n" +" /Contents 5 0 R\r\n" +" /Resources << /ProcSet 7 0 R\r\n" +" /Font << /F1 8 0 R >>\r\n" +" >>\r\n" +" >>\r\n" +"endobj\r\n", + MmToPts(ptMax.x - ptMin.x), + MmToPts(ptMax.y - ptMin.y)); + + xref[5] = (uint32_t)ftell(f); + fprintf(f, +"5 0 obj\r\n" +" << /Length 6 0 R >>\r\n" +"stream\r\n"); + bodyStart = (uint32_t)ftell(f); +} + +void PdfFileWriter::FinishAndCloseFile() { + uint32_t bodyEnd = (uint32_t)ftell(f); + + fprintf(f, +"endstream\r\n" +"endobj\r\n"); + + xref[6] = (uint32_t)ftell(f); + fprintf(f, +"6 0 obj\r\n" +" %d\r\n" +"endobj\r\n", + bodyEnd - bodyStart); + + xref[7] = (uint32_t)ftell(f); + fprintf(f, +"7 0 obj\r\n" +" [/PDF /Text]\r\n" +"endobj\r\n"); + + xref[8] = (uint32_t)ftell(f); + fprintf(f, +"8 0 obj\r\n" +" << /Type /Font\r\n" +" /Subtype /Type1\r\n" +" /Name /F1\r\n" +" /BaseFont /Helvetica\r\n" +" /Encoding /WinAnsiEncoding\r\n" +" >>\r\n" +"endobj\r\n"); + + xref[9] = (uint32_t)ftell(f); + fprintf(f, +"9 0 obj\r\n" +" << /Creator (SolveSpace)\r\n" +" >>\r\n"); + + uint32_t xrefStart = (uint32_t)ftell(f); + fprintf(f, +"xref\r\n" +"0 10\r\n" +"0000000000 65535 f\r\n"); + + int i; + for(i = 1; i <= 9; i++) { + fprintf(f, "%010d %05d n\r\n", xref[i], 0); + } + + fprintf(f, +"\r\n" +"trailer\r\n" +" << /Size 10\r\n" +" /Root 1 0 R\r\n" +" /Info 9 0 R\r\n" +" >>\r\n" +"startxref\r\n" +"%d\r\n" +"%%%%EOF\r\n", + xrefStart); + + fclose(f); + +} + +void PdfFileWriter::Background(RgbaColor color) { + double width = ptMax.x - ptMin.x; + double height = ptMax.y - ptMin.y; + double sw = max(width, height) / 1000; + + fprintf(f, +"1 J 1 j\r\n" +"%.3f %.3f %.3f RG\r\n" +"%.3f %.3f %.3f rg\r\n" +"%.3f w\r\n" +"%.3f %.3f m\r\n" +"%.3f %.3f l\r\n" +"%.3f %.3f l\r\n" +"%.3f %.3f l\r\n" +"b\r\n", + color.redF(), color.greenF(), color.blueF(), + color.redF(), color.greenF(), color.blueF(), + MmToPts(sw), + MmToPts(0), MmToPts(0), + MmToPts(width), MmToPts(0), + MmToPts(width), MmToPts(height), + MmToPts(0), MmToPts(height)); +} + +void PdfFileWriter::StartPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) +{ + StipplePattern pattern = Style::PatternType(hs); + double stippleScale = MmToPts(Style::StippleScaleMm(hs)); + + fprintf(f, "1 J 1 j " // round endcaps and joins + "%.3f w [%s] 0 d " + "%.3f %.3f %.3f RG\r\n", + MmToPts(lineWidth), + MakeStipplePattern(pattern, stippleScale, ' ').c_str(), + strokeRgb.redF(), strokeRgb.greenF(), strokeRgb.blueF()); + if(filled) { + fprintf(f, "%.3f %.3f %.3f rg\r\n", + fillRgb.redF(), fillRgb.greenF(), fillRgb.blueF()); + } + + prevPt = Vector::From(VERY_POSITIVE, VERY_POSITIVE, VERY_POSITIVE); +} +void PdfFileWriter::FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) +{ + if(filled) { + fprintf(f, "b\r\n"); + } else { + fprintf(f, "S\r\n"); + } +} + +void PdfFileWriter::MaybeMoveTo(Vector st, Vector fi) { + if(!prevPt.Equals(st)) { + fprintf(f, "%.3f %.3f m\r\n", + MmToPts(st.x - ptMin.x), MmToPts(st.y - ptMin.y)); + } + prevPt = fi; +} + +void PdfFileWriter::Triangle(STriangle *tr) { + double sw = max(ptMax.x - ptMin.x, ptMax.y - ptMin.y) / 1000; + + fprintf(f, +"1 J 1 j\r\n" +"%.3f %.3f %.3f RG\r\n" +"%.3f %.3f %.3f rg\r\n" +"%.3f w\r\n" +"%.3f %.3f m\r\n" +"%.3f %.3f l\r\n" +"%.3f %.3f l\r\n" +"b\r\n", + tr->meta.color.redF(), tr->meta.color.greenF(), tr->meta.color.blueF(), + tr->meta.color.redF(), tr->meta.color.greenF(), tr->meta.color.blueF(), + MmToPts(sw), + MmToPts(tr->a.x - ptMin.x), MmToPts(tr->a.y - ptMin.y), + MmToPts(tr->b.x - ptMin.x), MmToPts(tr->b.y - ptMin.y), + MmToPts(tr->c.x - ptMin.x), MmToPts(tr->c.y - ptMin.y)); +} + +void PdfFileWriter::Bezier(SBezier *sb) { + if(sb->deg == 1) { + MaybeMoveTo(sb->ctrl[0], sb->ctrl[1]); + fprintf(f, +"%.3f %.3f l\r\n", + MmToPts(sb->ctrl[1].x - ptMin.x), MmToPts(sb->ctrl[1].y - ptMin.y)); + } else if(sb->deg == 3 && !sb->IsRational()) { + MaybeMoveTo(sb->ctrl[0], sb->ctrl[3]); + fprintf(f, +"%.3f %.3f %.3f %.3f %.3f %.3f c\r\n", + MmToPts(sb->ctrl[1].x - ptMin.x), MmToPts(sb->ctrl[1].y - ptMin.y), + MmToPts(sb->ctrl[2].x - ptMin.x), MmToPts(sb->ctrl[2].y - ptMin.y), + MmToPts(sb->ctrl[3].x - ptMin.x), MmToPts(sb->ctrl[3].y - ptMin.y)); + } else { + BezierAsNonrationalCubic(sb); + } +} + +//----------------------------------------------------------------------------- +// Routines for SVG output +//----------------------------------------------------------------------------- +void SvgFileWriter::StartFile() { + fprintf(f, +"\r\n" +"\r\n" +"\r\n" +"Exported SVG\r\n" +"\r\n", + (ptMax.x - ptMin.x), (ptMax.y - ptMin.y), + (ptMax.x - ptMin.x), (ptMax.y - ptMin.y)); + + fprintf(f, "\r\n"); +} + +void SvgFileWriter::Background(RgbaColor color) { + fprintf(f, +"\r\n", + color.red, color.green, color.blue); +} + +void SvgFileWriter::StartPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) +{ + fprintf(f, "\r\n", cls.c_str(), fill.c_str()); +} + +void SvgFileWriter::MaybeMoveTo(Vector st, Vector fi) { + // SVG uses a coordinate system with the origin at top left, +y down + if(!prevPt.Equals(st)) { + fprintf(f, "M%.3f %.3f ", (st.x - ptMin.x), (ptMax.y - st.y)); + } + prevPt = fi; +} + +void SvgFileWriter::Triangle(STriangle *tr) { + fprintf(f, +"\r\n", + (tr->a.x - ptMin.x), (ptMax.y - tr->a.y), + (tr->b.x - ptMin.x), (ptMax.y - tr->b.y), + (tr->c.x - ptMin.x), (ptMax.y - tr->c.y), + tr->meta.color.red, tr->meta.color.green, tr->meta.color.blue, + tr->meta.color.red, tr->meta.color.green, tr->meta.color.blue); +} + +void SvgFileWriter::Bezier(SBezier *sb) { + Vector c, n = Vector::From(0, 0, 1); + double r; + if(sb->deg == 1) { + MaybeMoveTo(sb->ctrl[0], sb->ctrl[1]); + fprintf(f, "L%.3f,%.3f ", + (sb->ctrl[1].x - ptMin.x), (ptMax.y - sb->ctrl[1].y)); + } else if(sb->IsCircle(n, &c, &r)) { + Vector p0 = sb->ctrl[0], p1 = sb->ctrl[2]; + double theta0 = atan2(p0.y - c.y, p0.x - c.x), + theta1 = atan2(p1.y - c.y, p1.x - c.x), + dtheta = WRAP_SYMMETRIC(theta1 - theta0, 2*PI); + // The arc must be less than 180 degrees, or else it couldn't have + // been represented as a single rational Bezier. So large-arc-flag + // must be false. sweep-flag is determined by the sign of dtheta. + // Note that clockwise and counter-clockwise are backwards in SVG's + // mirrored csys. + MaybeMoveTo(p0, p1); + fprintf(f, "A%.3f,%.3f 0 0,%d %.3f,%.3f ", + r, r, + (dtheta < 0) ? 1 : 0, + p1.x - ptMin.x, ptMax.y - p1.y); + } else if(!sb->IsRational()) { + if(sb->deg == 2) { + MaybeMoveTo(sb->ctrl[0], sb->ctrl[2]); + fprintf(f, "Q%.3f,%.3f %.3f,%.3f ", + sb->ctrl[1].x - ptMin.x, ptMax.y - sb->ctrl[1].y, + sb->ctrl[2].x - ptMin.x, ptMax.y - sb->ctrl[2].y); + } else if(sb->deg == 3) { + MaybeMoveTo(sb->ctrl[0], sb->ctrl[3]); + fprintf(f, "C%.3f,%.3f %.3f,%.3f %.3f,%.3f ", + sb->ctrl[1].x - ptMin.x, ptMax.y - sb->ctrl[1].y, + sb->ctrl[2].x - ptMin.x, ptMax.y - sb->ctrl[2].y, + sb->ctrl[3].x - ptMin.x, ptMax.y - sb->ctrl[3].y); + } + } else { + BezierAsNonrationalCubic(sb); + } +} + +void SvgFileWriter::FinishAndCloseFile() { + fprintf(f, "\r\n\r\n"); + fclose(f); +} + +//----------------------------------------------------------------------------- +// Routines for HPGL output +//----------------------------------------------------------------------------- +double HpglFileWriter::MmToHpglUnits(double mm) { + return mm*40; +} + +void HpglFileWriter::StartFile() { + fprintf(f, "IN;\r\n"); + fprintf(f, "SP1;\r\n"); +} + +void HpglFileWriter::Background(RgbaColor color) { +} + +void HpglFileWriter::StartPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) +{ +} +void HpglFileWriter::FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) +{ +} + +void HpglFileWriter::Triangle(STriangle *tr) { +} + +void HpglFileWriter::Bezier(SBezier *sb) { + if(sb->deg == 1) { + fprintf(f, "PU%d,%d;\r\n", + (int)MmToHpglUnits(sb->ctrl[0].x), + (int)MmToHpglUnits(sb->ctrl[0].y)); + fprintf(f, "PD%d,%d;\r\n", + (int)MmToHpglUnits(sb->ctrl[1].x), + (int)MmToHpglUnits(sb->ctrl[1].y)); + } else { + BezierAsPwl(sb); + } +} + +void HpglFileWriter::FinishAndCloseFile() { + fclose(f); +} + +//----------------------------------------------------------------------------- +// Routines for G Code output. Slightly complicated by our ability to generate +// multiple passes, and to specify the feeds and depth; those parameters get +// set in the configuration screen. +//----------------------------------------------------------------------------- +void GCodeFileWriter::StartFile() { + sel = {}; +} +void GCodeFileWriter::StartPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) +{ +} +void GCodeFileWriter::Background(RgbaColor color) { +} +void GCodeFileWriter::FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) +{ +} +void GCodeFileWriter::Triangle(STriangle *tr) { +} + +void GCodeFileWriter::Bezier(SBezier *sb) { + if(sb->deg == 1) { + sel.AddEdge(sb->ctrl[0], sb->ctrl[1]); + } else { + BezierAsPwl(sb); + } +} + +void GCodeFileWriter::FinishAndCloseFile() { + SPolygon sp = {}; + sel.AssemblePolygon(&sp, NULL); + + int i; + for(i = 0; i < SS.gCode.passes; i++) { + double depth = (SS.gCode.depth / SS.gCode.passes)*(i+1); + + SContour *sc; + for(sc = sp.l.First(); sc; sc = sp.l.NextAfter(sc)) { + if(sc->l.n < 2) continue; + + SPoint *pt = sc->l.First(); + fprintf(f, "G00 X%s Y%s\r\n", + SS.MmToString(pt->p.x).c_str(), SS.MmToString(pt->p.y).c_str()); + fprintf(f, "G01 Z%s F%s\r\n", + SS.MmToString(depth).c_str(), SS.MmToString(SS.gCode.plungeFeed).c_str()); + + pt = sc->l.NextAfter(pt); + for(; pt; pt = sc->l.NextAfter(pt)) { + fprintf(f, "G01 X%s Y%s F%s\r\n", + SS.MmToString(pt->p.x).c_str(), SS.MmToString(pt->p.y).c_str(), + SS.MmToString(SS.gCode.feed).c_str()); + } + // Move up to a clearance plane above the work. + fprintf(f, "G00 Z%s\r\n", + SS.MmToString(SS.gCode.safeHeight).c_str()); + } + } + + sp.Clear(); + sel.Clear(); + fclose(f); +} + + +//----------------------------------------------------------------------------- +// Routine for STEP output; just a wrapper around the general STEP stuff that +// can also be used for surfaces or 3d curves. +//----------------------------------------------------------------------------- +void Step2dFileWriter::StartFile() { + sfw = {}; + sfw.f = f; + sfw.WriteHeader(); +} + +void Step2dFileWriter::Background(RgbaColor color) { +} + +void Step2dFileWriter::Triangle(STriangle *tr) { +} + +void Step2dFileWriter::StartPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) +{ +} +void Step2dFileWriter::FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) +{ +} + +void Step2dFileWriter::Bezier(SBezier *sb) { + int c = sfw.ExportCurve(sb); + sfw.curves.Add(&c); +} + +void Step2dFileWriter::FinishAndCloseFile() { + sfw.WriteWireframe(); + sfw.WriteFooter(); + fclose(f); +} + +} // namespace SolveSpace diff --git a/src/expr.cpp b/src/expr.cpp new file mode 100644 index 0000000..6b077fa --- /dev/null +++ b/src/expr.cpp @@ -0,0 +1,963 @@ +//----------------------------------------------------------------------------- +// The symbolic algebra system used to write our constraint equations; +// routines to build expressions in software or from a user-provided string, +// and to compute the partial derivatives that we'll use when write our +// Jacobian matrix. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +static inline Expr *AllocExpr() { + return (Expr *)Platform::AllocTemporary(sizeof(Expr)); +} + +ExprVector ExprVector::From(Expr *x, Expr *y, Expr *z) { + ExprVector r = { x, y, z}; + return r; +} + +ExprVector ExprVector::From(Vector vn) { + ExprVector ve; + ve.x = Expr::From(vn.x); + ve.y = Expr::From(vn.y); + ve.z = Expr::From(vn.z); + return ve; +} + +ExprVector ExprVector::From(hParam x, hParam y, hParam z) { + ExprVector ve; + ve.x = Expr::From(x); + ve.y = Expr::From(y); + ve.z = Expr::From(z); + return ve; +} + +ExprVector ExprVector::From(double x, double y, double z) { + ExprVector ve; + ve.x = Expr::From(x); + ve.y = Expr::From(y); + ve.z = Expr::From(z); + return ve; +} + +ExprVector ExprVector::Minus(ExprVector b) const { + ExprVector r; + r.x = x->Minus(b.x); + r.y = y->Minus(b.y); + r.z = z->Minus(b.z); + return r; +} + +ExprVector ExprVector::Plus(ExprVector b) const { + ExprVector r; + r.x = x->Plus(b.x); + r.y = y->Plus(b.y); + r.z = z->Plus(b.z); + return r; +} + +Expr *ExprVector::Dot(ExprVector b) const { + Expr *r; + r = x->Times(b.x); + r = r->Plus(y->Times(b.y)); + r = r->Plus(z->Times(b.z)); + return r; +} + +ExprVector ExprVector::Cross(ExprVector b) const { + ExprVector r; + r.x = (y->Times(b.z))->Minus(z->Times(b.y)); + r.y = (z->Times(b.x))->Minus(x->Times(b.z)); + r.z = (x->Times(b.y))->Minus(y->Times(b.x)); + return r; +} + +ExprVector ExprVector::ScaledBy(Expr *s) const { + ExprVector r; + r.x = x->Times(s); + r.y = y->Times(s); + r.z = z->Times(s); + return r; +} + +ExprVector ExprVector::WithMagnitude(Expr *s) const { + Expr *m = Magnitude(); + return ScaledBy(s->Div(m)); +} + +Expr *ExprVector::Magnitude() const { + Expr *r; + r = x->Square(); + r = r->Plus(y->Square()); + r = r->Plus(z->Square()); + return r->Sqrt(); +} + +Vector ExprVector::Eval() const { + Vector r; + r.x = x->Eval(); + r.y = y->Eval(); + r.z = z->Eval(); + return r; +} + +ExprQuaternion ExprQuaternion::From(hParam w, hParam vx, hParam vy, hParam vz) { + ExprQuaternion q; + q.w = Expr::From(w); + q.vx = Expr::From(vx); + q.vy = Expr::From(vy); + q.vz = Expr::From(vz); + return q; +} + +ExprQuaternion ExprQuaternion::From(Expr *w, Expr *vx, Expr *vy, Expr *vz) +{ + ExprQuaternion q; + q.w = w; + q.vx = vx; + q.vy = vy; + q.vz = vz; + return q; +} + +ExprQuaternion ExprQuaternion::From(Quaternion qn) { + ExprQuaternion qe; + qe.w = Expr::From(qn.w); + qe.vx = Expr::From(qn.vx); + qe.vy = Expr::From(qn.vy); + qe.vz = Expr::From(qn.vz); + return qe; +} + +ExprVector ExprQuaternion::RotationU() const { + ExprVector u; + Expr *two = Expr::From(2); + + u.x = w->Square(); + u.x = (u.x)->Plus(vx->Square()); + u.x = (u.x)->Minus(vy->Square()); + u.x = (u.x)->Minus(vz->Square()); + + u.y = two->Times(w->Times(vz)); + u.y = (u.y)->Plus(two->Times(vx->Times(vy))); + + u.z = two->Times(vx->Times(vz)); + u.z = (u.z)->Minus(two->Times(w->Times(vy))); + + return u; +} + +ExprVector ExprQuaternion::RotationV() const { + ExprVector v; + Expr *two = Expr::From(2); + + v.x = two->Times(vx->Times(vy)); + v.x = (v.x)->Minus(two->Times(w->Times(vz))); + + v.y = w->Square(); + v.y = (v.y)->Minus(vx->Square()); + v.y = (v.y)->Plus(vy->Square()); + v.y = (v.y)->Minus(vz->Square()); + + v.z = two->Times(w->Times(vx)); + v.z = (v.z)->Plus(two->Times(vy->Times(vz))); + + return v; +} + +ExprVector ExprQuaternion::RotationN() const { + ExprVector n; + Expr *two = Expr::From(2); + + n.x = two->Times( w->Times(vy)); + n.x = (n.x)->Plus (two->Times(vx->Times(vz))); + + n.y = two->Times(vy->Times(vz)); + n.y = (n.y)->Minus(two->Times( w->Times(vx))); + + n.z = w->Square(); + n.z = (n.z)->Minus(vx->Square()); + n.z = (n.z)->Minus(vy->Square()); + n.z = (n.z)->Plus (vz->Square()); + + return n; +} + +ExprVector ExprQuaternion::Rotate(ExprVector p) const { + // Express the point in the new basis + return (RotationU().ScaledBy(p.x)).Plus( + RotationV().ScaledBy(p.y)).Plus( + RotationN().ScaledBy(p.z)); +} + +ExprQuaternion ExprQuaternion::Times(ExprQuaternion b) const { + Expr *sa = w, *sb = b.w; + ExprVector va = { vx, vy, vz }; + ExprVector vb = { b.vx, b.vy, b.vz }; + + ExprQuaternion r; + r.w = (sa->Times(sb))->Minus(va.Dot(vb)); + ExprVector vr = vb.ScaledBy(sa).Plus( + va.ScaledBy(sb).Plus( + va.Cross(vb))); + r.vx = vr.x; + r.vy = vr.y; + r.vz = vr.z; + return r; +} + +Expr *ExprQuaternion::Magnitude() const { + return ((w ->Square())->Plus( + (vx->Square())->Plus( + (vy->Square())->Plus( + (vz->Square())))))->Sqrt(); +} + + +Expr *Expr::From(hParam p) { + Expr *r = AllocExpr(); + r->op = Op::PARAM; + r->parh = p; + return r; +} + +Expr *Expr::From(double v) { + // Statically allocate common constants. + // Note: this is only valid because AllocExpr() uses AllocTemporary(), + // and Expr* is never explicitly freed. + + if(v == 0.0) { + static Expr zero(0.0); + return &zero; + } + + if(v == 1.0) { + static Expr one(1.0); + return &one; + } + + if(v == -1.0) { + static Expr mone(-1.0); + return &mone; + } + + if(v == 0.5) { + static Expr half(0.5); + return ½ + } + + if(v == -0.5) { + static Expr mhalf(-0.5); + return &mhalf; + } + + Expr *r = AllocExpr(); + r->op = Op::CONSTANT; + r->v = v; + return r; +} + +Expr *Expr::AnyOp(Op newOp, Expr *b) { + Expr *r = AllocExpr(); + r->op = newOp; + r->a = this; + r->b = b; + return r; +} + +int Expr::Children() const { + switch(op) { + case Op::PARAM: + case Op::PARAM_PTR: + case Op::CONSTANT: + case Op::VARIABLE: + return 0; + + case Op::PLUS: + case Op::MINUS: + case Op::TIMES: + case Op::DIV: + return 2; + + case Op::NEGATE: + case Op::SQRT: + case Op::SQUARE: + case Op::SIN: + case Op::COS: + case Op::ASIN: + case Op::ACOS: + return 1; + } + ssassert(false, "Unexpected operation"); +} + +int Expr::Nodes() const { + switch(Children()) { + case 0: return 1; + case 1: return 1 + a->Nodes(); + case 2: return 1 + a->Nodes() + b->Nodes(); + default: ssassert(false, "Unexpected children count"); + } +} + +Expr *Expr::DeepCopy() const { + Expr *n = AllocExpr(); + *n = *this; + int c = n->Children(); + if(c > 0) n->a = a->DeepCopy(); + if(c > 1) n->b = b->DeepCopy(); + return n; +} + +Expr *Expr::DeepCopyWithParamsAsPointers(ParamList *firstTry, ParamList *thenTry, + bool foldConstants) const { + Expr *n = AllocExpr(); + if(op == Op::PARAM) { + // A param that is referenced by its hParam gets rewritten to go + // straight in to the parameter table with a pointer, or simply + // into a constant if it's already known. + Param *p = firstTry->FindByIdNoOops(parh); + if(!p) p = thenTry->FindById(parh); + if(p->known) { + n->op = Op::CONSTANT; + n->v = p->val; + } else { + n->op = Op::PARAM_PTR; + n->parp = p; + } + return n; + } + + *n = *this; + int c = n->Children(); + if(c > 0) { + n->a = a->DeepCopyWithParamsAsPointers(firstTry, thenTry, foldConstants); + bool hasConstants = n->a->op == Op::CONSTANT; + if(c > 1) { + n->b = b->DeepCopyWithParamsAsPointers(firstTry, thenTry, foldConstants); + hasConstants |= n->b->op == Op::CONSTANT; + } + if(hasConstants && foldConstants) { + n = n->FoldConstants(false, 0); + } + } + return n; +} + +double Expr::Eval() const { + switch(op) { + case Op::PARAM: return SK.GetParam(parh)->val; + case Op::PARAM_PTR: return parp->val; + + case Op::CONSTANT: return v; + case Op::VARIABLE: ssassert(false, "Not supported yet"); + + case Op::PLUS: return a->Eval() + b->Eval(); + case Op::MINUS: return a->Eval() - b->Eval(); + case Op::TIMES: return a->Eval() * b->Eval(); + case Op::DIV: return a->Eval() / b->Eval(); + + case Op::NEGATE: return -(a->Eval()); + case Op::SQRT: return sqrt(a->Eval()); + case Op::SQUARE: { double r = a->Eval(); return r*r; } + case Op::SIN: return sin(a->Eval()); + case Op::COS: return cos(a->Eval()); + case Op::ACOS: return acos(a->Eval()); + case Op::ASIN: return asin(a->Eval()); + } + ssassert(false, "Unexpected operation"); +} + +Expr *Expr::PartialWrt(hParam p) const { + Expr *da, *db; + + switch(op) { + case Op::PARAM_PTR: return From(p == parp->h ? 1 : 0); + case Op::PARAM: return From(p == parh ? 1 : 0); + + case Op::CONSTANT: return From(0.0); + case Op::VARIABLE: ssassert(false, "Not supported yet"); + + case Op::PLUS: return (a->PartialWrt(p))->Plus(b->PartialWrt(p)); + case Op::MINUS: return (a->PartialWrt(p))->Minus(b->PartialWrt(p)); + + case Op::TIMES: + da = a->PartialWrt(p); + db = b->PartialWrt(p); + return (a->Times(db))->Plus(b->Times(da)); + + case Op::DIV: + da = a->PartialWrt(p); + db = b->PartialWrt(p); + return ((da->Times(b))->Minus(a->Times(db)))->Div(b->Square()); + + case Op::SQRT: + return (From(0.5)->Div(a->Sqrt()))->Times(a->PartialWrt(p)); + + case Op::SQUARE: + return (From(2.0)->Times(a))->Times(a->PartialWrt(p)); + + case Op::NEGATE: return (a->PartialWrt(p))->Negate(); + case Op::SIN: return (a->Cos())->Times(a->PartialWrt(p)); + case Op::COS: return ((a->Sin())->Times(a->PartialWrt(p)))->Negate(); + + case Op::ASIN: + return (From(1)->Div((From(1)->Minus(a->Square()))->Sqrt())) + ->Times(a->PartialWrt(p)); + case Op::ACOS: + return (From(-1)->Div((From(1)->Minus(a->Square()))->Sqrt())) + ->Times(a->PartialWrt(p)); + } + ssassert(false, "Unexpected operation"); +} + +void Expr::ParamsUsedList(ParamSet *list) const { + if(op == Op::PARAM || op == Op::PARAM_PTR) { + // leaf: just add ourselves if we aren't already there + hParam param = (op == Op::PARAM) ? parh : parp->h; + list->insert(param); + return; + } + + int c = Children(); + if(c >= 1) { + a->ParamsUsedList(list); + if(c >= 2) b->ParamsUsedList(list); + } +} + +bool Expr::DependsOn(hParam p) const { + if(op == Op::PARAM) return (parh == p); + if(op == Op::PARAM_PTR) return (parp->h == p); + + int c = Children(); + if(c == 1) return a->DependsOn(p); + if(c == 2) return a->DependsOn(p) || b->DependsOn(p); + return false; +} + +bool Expr::Tol(double a, double b) { + return fabs(a - b) < 0.001; +} + +bool Expr::IsZeroConst() const { + return op == Op::CONSTANT && EXACT(v == 0.0); +} + +Expr *Expr::FoldConstants(bool allocCopy, size_t depth) { + Expr *n; + + if(!allocCopy) { + n = this; + } else { + n = AllocExpr(); + *n = *this; + } + + switch(op) { + case Op::PARAM_PTR: + case Op::PARAM: + case Op::CONSTANT: + case Op::VARIABLE: + break; + + case Op::MINUS: + case Op::TIMES: + case Op::DIV: + case Op::PLUS: + if(depth > 0) { + n->a = a->FoldConstants(allocCopy, depth - 1); + n->b = b->FoldConstants(allocCopy, depth - 1); + } + + // If both ops are known, then we can evaluate immediately + if(n->a->op == Op::CONSTANT && n->b->op == Op::CONSTANT) { + double nv = n->Eval(); + n->op = Op::CONSTANT; + n->v = nv; + break; + } + // x + 0 = 0 + x = x + if(op == Op::PLUS && n->b->op == Op::CONSTANT && Tol(n->b->v, 0)) { + *n = *(n->a); break; + } + if(op == Op::PLUS && n->a->op == Op::CONSTANT && Tol(n->a->v, 0)) { + *n = *(n->b); break; + } + // 1*x = x*1 = x + if(op == Op::TIMES && n->b->op == Op::CONSTANT && Tol(n->b->v, 1)) { + *n = *(n->a); break; + } + if(op == Op::TIMES && n->a->op == Op::CONSTANT && Tol(n->a->v, 1)) { + *n = *(n->b); break; + } + // 0*x = x*0 = 0 + if(op == Op::TIMES && n->b->op == Op::CONSTANT && Tol(n->b->v, 0)) { + n->op = Op::CONSTANT; n->v = 0; break; + } + if(op == Op::TIMES && n->a->op == Op::CONSTANT && Tol(n->a->v, 0)) { + n->op = Op::CONSTANT; n->v = 0; break; + } + + break; + + case Op::SQRT: + case Op::SQUARE: + case Op::NEGATE: + case Op::SIN: + case Op::COS: + case Op::ASIN: + case Op::ACOS: + if(depth > 0) { + n->a = a->FoldConstants(allocCopy, depth - 1); + } + + if(n->a->op == Op::CONSTANT) { + double nv = n->Eval(); + n->op = Op::CONSTANT; + n->v = nv; + } + break; + } + return n; +} + +void Expr::Substitute(const SubstitutionMap &subMap) { + ssassert(op != Op::PARAM_PTR, "Expected an expression that refer to params via handles"); + + if(op == Op::PARAM) { + auto it = subMap.find(parh); + if(it != subMap.end()) { + parh = it->second->h; + } + } else { + int c = Children(); + if(c >= 1) { + a->Substitute(subMap); + if(c >= 2) b->Substitute(subMap); + } + } +} + +//----------------------------------------------------------------------------- +// If the expression references only one parameter that appears in pl, then +// return that parameter. If no param is referenced, then return NO_PARAMS. +// If multiple params are referenced, then return MULTIPLE_PARAMS. +//----------------------------------------------------------------------------- +const hParam Expr::NO_PARAMS = { 0 }; +const hParam Expr::MULTIPLE_PARAMS = { std::numeric_limits::max() }; +hParam Expr::ReferencedParams(ParamList *pl) const { + if(op == Op::PARAM) { + if(pl->FindByIdNoOops(parh)) { + return parh; + } else { + return NO_PARAMS; + } + } + ssassert(op != Op::PARAM_PTR, "Expected an expression that refer to params via handles"); + + int c = Children(); + if(c == 0) { + return NO_PARAMS; + } else if(c == 1) { + return a->ReferencedParams(pl); + } else if(c == 2) { + hParam pa, pb; + pa = a->ReferencedParams(pl); + pb = b->ReferencedParams(pl); + if(pa == NO_PARAMS) { + return pb; + } else if(pb == NO_PARAMS) { + return pa; + } else if(pa == pb) { + return pa; // either, doesn't matter + } else { + return MULTIPLE_PARAMS; + } + } else ssassert(false, "Unexpected children count"); +} + + +//----------------------------------------------------------------------------- +// Routines to pretty-print an expression. Mostly for debugging. +//----------------------------------------------------------------------------- + +std::string Expr::Print() const { + char c; + switch(op) { + case Op::PARAM: return ssprintf("param(%08x)", parh.v); + case Op::PARAM_PTR: return ssprintf("param(p%08x)", parp->h.v); + + case Op::CONSTANT: return ssprintf("%.3f", v); + case Op::VARIABLE: return "(var)"; + + case Op::PLUS: c = '+'; goto p; + case Op::MINUS: c = '-'; goto p; + case Op::TIMES: c = '*'; goto p; + case Op::DIV: c = '/'; goto p; +p: + return "(" + a->Print() + " " + c + " " + b->Print() + ")"; + break; + + case Op::NEGATE: return "(- " + a->Print() + ")"; + case Op::SQRT: return "(sqrt " + a->Print() + ")"; + case Op::SQUARE: return "(square " + a->Print() + ")"; + case Op::SIN: return "(sin " + a->Print() + ")"; + case Op::COS: return "(cos " + a->Print() + ")"; + case Op::ASIN: return "(asin " + a->Print() + ")"; + case Op::ACOS: return "(acos " + a->Print() + ")"; + } + ssassert(false, "Unexpected operation"); +} + + +//----------------------------------------------------------------------------- +// A parser; convert a string to an expression. Infix notation, with the +// usual shift/reduce approach. I had great hopes for user-entered eq +// constraints, but those don't seem very useful, so right now this is just +// to provide calculator type functionality wherever numbers are entered. +//----------------------------------------------------------------------------- + +class ExprParser { +public: + enum class TokenType { + ERROR = 0, + + PAREN_LEFT, + PAREN_RIGHT, + BINARY_OP, + UNARY_OP, + OPERAND, + + END, + }; + + class Token { + public: + TokenType type; + Expr *expr; + + static Token From(TokenType type = TokenType::ERROR, Expr *expr = NULL); + static Token From(TokenType type, Expr::Op op); + bool IsError() const { return type == TokenType::ERROR; } + }; + + std::string::const_iterator it, end; + std::vector stack; + + char ReadChar(); + char PeekChar(); + + std::string ReadWord(); + void SkipSpace(); + + Token PopOperator(std::string *error); + Token PopOperand(std::string *error); + + int Precedence(Token token); + Token LexNumber(std::string *error); + Token Lex(std::string *error); + bool Reduce(std::string *error); + bool Parse(std::string *error, size_t reduceUntil = 0); + + static Expr *Parse(const std::string &input, std::string *error); +}; + +ExprParser::Token ExprParser::Token::From(TokenType type, Expr *expr) { + Token t; + t.type = type; + t.expr = expr; + return t; +} + +ExprParser::Token ExprParser::Token::From(TokenType type, Expr::Op op) { + Token t; + t.type = type; + t.expr = AllocExpr(); + t.expr->op = op; + return t; +} + +char ExprParser::ReadChar() { + return *it++; +} + +char ExprParser::PeekChar() { + if(it == end) { + return '\0'; + } else { + return *it; + } +} + +std::string ExprParser::ReadWord() { + std::string s; + + while(char c = PeekChar()) { + if(!isalnum(c)) break; + s.push_back(ReadChar()); + } + + return s; +} + +void ExprParser::SkipSpace() { + while(char c = PeekChar()) { + if(!isspace(c)) break; + ReadChar(); + } +} + +ExprParser::Token ExprParser::LexNumber(std::string *error) { + std::string s; + + while(char c = PeekChar()) { + if(!((c >= '0' && c <= '9') || c == 'e' || c == 'E' || c == '.' || c == '_')) break; + if(c == '_') { + ReadChar(); + continue; + } + s.push_back(ReadChar()); + } + + char *endptr; + double d = strtod(s.c_str(), &endptr); + + Token t = Token::From(); + if(endptr == s.c_str() + s.size()) { + t = Token::From(TokenType::OPERAND, Expr::Op::CONSTANT); + t.expr->v = d; + } else { + *error = "'" + s + "' is not a valid number"; + } + return t; +} + +ExprParser::Token ExprParser::Lex(std::string *error) { + SkipSpace(); + + Token t = Token::From(); + char c = PeekChar(); + if(isupper(c)) { + std::string n = ReadWord(); + t = Token::From(TokenType::OPERAND, Expr::Op::VARIABLE); + } else if(isalpha(c)) { + std::string s = ReadWord(); + if(s == "sqrt") { + t = Token::From(TokenType::UNARY_OP, Expr::Op::SQRT); + } else if(s == "square") { + t = Token::From(TokenType::UNARY_OP, Expr::Op::SQUARE); + } else if(s == "sin") { + t = Token::From(TokenType::UNARY_OP, Expr::Op::SIN); + } else if(s == "cos") { + t = Token::From(TokenType::UNARY_OP, Expr::Op::COS); + } else if(s == "asin") { + t = Token::From(TokenType::UNARY_OP, Expr::Op::ASIN); + } else if(s == "acos") { + t = Token::From(TokenType::UNARY_OP, Expr::Op::ACOS); + } else if(s == "pi") { + t = Token::From(TokenType::OPERAND, Expr::Op::CONSTANT); + t.expr->v = PI; + } else { + *error = "'" + s + "' is not a valid variable, function or constant"; + } + } else if(isdigit(c) || c == '.') { + return LexNumber(error); + } else if(ispunct(c)) { + ReadChar(); + if(c == '+') { + t = Token::From(TokenType::BINARY_OP, Expr::Op::PLUS); + } else if(c == '-') { + t = Token::From(TokenType::BINARY_OP, Expr::Op::MINUS); + } else if(c == '*') { + t = Token::From(TokenType::BINARY_OP, Expr::Op::TIMES); + } else if(c == '/') { + t = Token::From(TokenType::BINARY_OP, Expr::Op::DIV); + } else if(c == '(') { + t = Token::From(TokenType::PAREN_LEFT); + } else if(c == ')') { + t = Token::From(TokenType::PAREN_RIGHT); + } else { + *error = "'" + std::string(1, c) + "' is not a valid operator"; + } + } else if(c == '\0') { + t = Token::From(TokenType::END); + } else { + *error = "Unexpected character '" + std::string(1, c) + "'"; + } + + return t; +} + +ExprParser::Token ExprParser::PopOperand(std::string *error) { + Token t = Token::From(); + if(stack.empty() || stack.back().type != TokenType::OPERAND) { + *error = "Expected an operand"; + } else { + t = stack.back(); + stack.pop_back(); + } + return t; +} + +ExprParser::Token ExprParser::PopOperator(std::string *error) { + Token t = Token::From(); + if(stack.empty() || (stack.back().type != TokenType::UNARY_OP && + stack.back().type != TokenType::BINARY_OP)) { + *error = "Expected an operator"; + } else { + t = stack.back(); + stack.pop_back(); + } + return t; +} + +int ExprParser::Precedence(Token t) { + ssassert(t.type == TokenType::BINARY_OP || + t.type == TokenType::UNARY_OP || + t.type == TokenType::OPERAND, + "Unexpected token type"); + + if(t.type == TokenType::UNARY_OP) { + return 30; + } else if(t.expr->op == Expr::Op::TIMES || + t.expr->op == Expr::Op::DIV) { + return 20; + } else if(t.expr->op == Expr::Op::PLUS || + t.expr->op == Expr::Op::MINUS) { + return 10; + } else if(t.type == TokenType::OPERAND) { + return 0; + } else ssassert(false, "Unexpected operator"); +} + +bool ExprParser::Reduce(std::string *error) { + Token a = PopOperand(error); + if(a.IsError()) return false; + + Token op = PopOperator(error); + if(op.IsError()) return false; + + Token r = Token::From(TokenType::OPERAND); + switch(op.type) { + case TokenType::BINARY_OP: { + Token b = PopOperand(error); + if(b.IsError()) return false; + r.expr = b.expr->AnyOp(op.expr->op, a.expr); + break; + } + + case TokenType::UNARY_OP: { + Expr *e = a.expr; + switch(op.expr->op) { + case Expr::Op::NEGATE: e = e->Negate(); break; + case Expr::Op::SQRT: e = e->Sqrt(); break; + case Expr::Op::SQUARE: e = e->Times(e); break; + case Expr::Op::SIN: e = e->Times(Expr::From(PI/180))->Sin(); break; + case Expr::Op::COS: e = e->Times(Expr::From(PI/180))->Cos(); break; + case Expr::Op::ASIN: e = e->ASin()->Times(Expr::From(180/PI)); break; + case Expr::Op::ACOS: e = e->ACos()->Times(Expr::From(180/PI)); break; + default: ssassert(false, "Unexpected unary operator"); + } + r.expr = e; + break; + } + + default: ssassert(false, "Unexpected operator"); + } + stack.push_back(r); + + return true; +} + +bool ExprParser::Parse(std::string *error, size_t reduceUntil) { + while(true) { + Token t = Lex(error); + switch(t.type) { + case TokenType::ERROR: + return false; + + case TokenType::END: + case TokenType::PAREN_RIGHT: + while(stack.size() > 1 + reduceUntil) { + if(!Reduce(error)) return false; + } + + if(t.type == TokenType::PAREN_RIGHT) { + stack.push_back(t); + } + return true; + + case TokenType::PAREN_LEFT: { + // sub-expression + if(!Parse(error, /*reduceUntil=*/stack.size())) return false; + + if(stack.empty() || stack.back().type != TokenType::PAREN_RIGHT) { + *error = "Expected ')'"; + return false; + } + stack.pop_back(); + break; + } + + case TokenType::BINARY_OP: + if((stack.size() > reduceUntil && stack.back().type != TokenType::OPERAND) || + stack.size() == reduceUntil) { + if(t.expr->op == Expr::Op::MINUS) { + t.type = TokenType::UNARY_OP; + t.expr->op = Expr::Op::NEGATE; + stack.push_back(t); + break; + } + } + + while(stack.size() > 1 + reduceUntil && + Precedence(t) <= Precedence(stack[stack.size() - 2])) { + if(!Reduce(error)) return false; + } + + stack.push_back(t); + break; + + case TokenType::UNARY_OP: + case TokenType::OPERAND: + stack.push_back(t); + break; + } + } + + return true; +} + +Expr *ExprParser::Parse(const std::string &input, std::string *error) { + ExprParser parser; + parser.it = input.cbegin(); + parser.end = input.cend(); + if(!parser.Parse(error)) return NULL; + + Token r = parser.PopOperand(error); + if(r.IsError()) return NULL; + return r.expr; +} + +Expr *Expr::Parse(const std::string &input, std::string *error) { + return ExprParser::Parse(input, error); +} + +Expr *Expr::From(const std::string &input, bool popUpError) { + std::string error; + Expr *e = ExprParser::Parse(input, &error); + if(!e) { + dbp("Parse/lex error: %s", error.c_str()); + if(popUpError) { + Error("Not a valid number or expression: '%s'.\n%s.", + input.c_str(), error.c_str()); + } + } + return e; +} + +} // namespace SolveSpace diff --git a/src/expr.h b/src/expr.h new file mode 100644 index 0000000..eb01917 --- /dev/null +++ b/src/expr.h @@ -0,0 +1,154 @@ +//----------------------------------------------------------------------------- +// An expression in our symbolic algebra system, used to write, linearize, +// and solve our constraint equations. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#ifndef SOLVESPACE_EXPR_H +#define SOLVESPACE_EXPR_H + +#include +#include +#include +#include + +#include "dsc.h" +#include "param.h" + +namespace SolveSpace { + +using SubstitutionMap = std::unordered_map>; + +class Expr { +public: + + enum class Op : uint32_t { + // A parameter, by the hParam handle + PARAM = 0, + // A parameter, by a pointer straight in to the param table (faster, + // if we know that the param table won't move around) + PARAM_PTR = 1, + + // Operands + CONSTANT = 20, + VARIABLE = 21, + + // Binary ops + PLUS = 100, + MINUS = 101, + TIMES = 102, + DIV = 103, + // Unary ops + NEGATE = 104, + SQRT = 105, + SQUARE = 106, + SIN = 107, + COS = 108, + ASIN = 109, + ACOS = 110, + }; + + Op op; + Expr *a; + union { + double v; + hParam parh; + Param *parp; + Expr *b; + }; + + Expr() = default; + Expr(double val) : op(Op::CONSTANT) { v = val; } + + static Expr *From(hParam p); + static Expr *From(double v); + + Expr *AnyOp(Op op, Expr *b); + inline Expr *Plus (Expr *b_) { return AnyOp(Op::PLUS, b_); } + inline Expr *Minus(Expr *b_) { return AnyOp(Op::MINUS, b_); } + inline Expr *Times(Expr *b_) { return AnyOp(Op::TIMES, b_); } + inline Expr *Div (Expr *b_) { return AnyOp(Op::DIV, b_); } + + inline Expr *Negate() { return AnyOp(Op::NEGATE, NULL); } + inline Expr *Sqrt () { return AnyOp(Op::SQRT, NULL); } + inline Expr *Square() { return AnyOp(Op::SQUARE, NULL); } + inline Expr *Sin () { return AnyOp(Op::SIN, NULL); } + inline Expr *Cos () { return AnyOp(Op::COS, NULL); } + inline Expr *ASin () { return AnyOp(Op::ASIN, NULL); } + inline Expr *ACos () { return AnyOp(Op::ACOS, NULL); } + + Expr *PartialWrt(hParam p) const; + double Eval() const; + void ParamsUsedList(ParamSet *list) const; + bool DependsOn(hParam p) const; + static bool Tol(double a, double b); + bool IsZeroConst() const; + Expr *FoldConstants(bool allocCopy = true, size_t depth = std::numeric_limits::max()); + void Substitute(const SubstitutionMap &subMap); + + static const hParam NO_PARAMS, MULTIPLE_PARAMS; + hParam ReferencedParams(ParamList *pl) const; + + void ParamsToPointers(); + + std::string Print() const; + + // number of child nodes: 0 (e.g. constant), 1 (sqrt), or 2 (+) + int Children() const; + // total number of nodes in the tree + int Nodes() const; + + // Make a simple copy + Expr *DeepCopy() const; + // Make a copy, with the parameters (usually referenced by hParam) + // resolved to pointers to the actual value. This speeds things up + // considerably. + Expr *DeepCopyWithParamsAsPointers(ParamList *firstTry, + ParamList *thenTry, + bool foldConstants = false) const; + + static Expr *Parse(const std::string &input, std::string *error); + static Expr *From(const std::string &input, bool popUpError); +}; + +class ExprVector { +public: + Expr *x, *y, *z; + + static ExprVector From(Expr *x, Expr *y, Expr *z); + static ExprVector From(Vector vn); + static ExprVector From(hParam x, hParam y, hParam z); + static ExprVector From(double x, double y, double z); + + ExprVector Plus(ExprVector b) const; + ExprVector Minus(ExprVector b) const; + Expr *Dot(ExprVector b) const; + ExprVector Cross(ExprVector b) const; + ExprVector ScaledBy(Expr *s) const; + ExprVector WithMagnitude(Expr *s) const; + Expr *Magnitude() const; + + Vector Eval() const; +}; + +class ExprQuaternion { +public: + Expr *w, *vx, *vy, *vz; + + static ExprQuaternion From(Expr *w, Expr *vx, Expr *vy, Expr *vz); + static ExprQuaternion From(Quaternion qn); + static ExprQuaternion From(hParam w, hParam vx, hParam vy, hParam vz); + + ExprVector RotationU() const; + ExprVector RotationV() const; + ExprVector RotationN() const; + + ExprVector Rotate(ExprVector p) const; + ExprQuaternion Times(ExprQuaternion b) const; + + Expr *Magnitude() const; +}; + +} // namespace SolveSpace + +#endif diff --git a/src/file.cpp b/src/file.cpp new file mode 100644 index 0000000..86da16b --- /dev/null +++ b/src/file.cpp @@ -0,0 +1,1030 @@ +//----------------------------------------------------------------------------- +// Routines to write and read our .slvs file format. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +#define VERSION_STRING "\261\262\263" "SolveSpaceREVa" + +static int StrStartsWith(const char *str, const char *start) { + return memcmp(str, start, strlen(start)) == 0; +} + +//----------------------------------------------------------------------------- +// Clear and free all the dynamic memory associated with our currently-loaded +// sketch. This does not leave the program in an acceptable state (with the +// references created, and so on), so anyone calling this must fix that later. +//----------------------------------------------------------------------------- +void SolveSpaceUI::ClearExisting() { + UndoClearStack(&redo); + UndoClearStack(&undo); + + for(hGroup hg : SK.groupOrder) { + Group *g = SK.GetGroup(hg); + g->Clear(); + } + + SK.constraint.Clear(); + SK.request.Clear(); + SK.group.Clear(); + SK.groupOrder.Clear(); + SK.style.Clear(); + + SK.entity.Clear(); + SK.param.Clear(); + images.clear(); +} + +hGroup SolveSpaceUI::CreateDefaultDrawingGroup() { + Group g = {}; + + // And an empty group, for the first stuff the user draws. + g.visible = true; + g.name = C_("group-name", "sketch-in-plane"); + g.type = Group::Type::DRAWING_WORKPLANE; + g.subtype = Group::Subtype::WORKPLANE_BY_POINT_ORTHO; + g.order = 1; + g.predef.q = Quaternion::From(1, 0, 0, 0); + hRequest hr = Request::HREQUEST_REFERENCE_XY; + g.predef.origin = hr.entity(1); + SK.group.AddAndAssignId(&g); + SK.GetGroup(g.h)->activeWorkplane = g.h.entity(0); + return g.h; +} + +void SolveSpaceUI::NewFile() { + ClearExisting(); + + // Our initial group, that contains the references. + Group g = {}; + g.visible = true; + g.name = C_("group-name", "#references"); + g.type = Group::Type::DRAWING_3D; + g.order = 0; + g.h = Group::HGROUP_REFERENCES; + SK.group.Add(&g); + + // Let's create three two-d coordinate systems, for the coordinate + // planes; these are our references, present in every sketch. + Request r = {}; + r.type = Request::Type::WORKPLANE; + r.group = Group::HGROUP_REFERENCES; + r.workplane = Entity::FREE_IN_3D; + + r.h = Request::HREQUEST_REFERENCE_XY; + SK.request.Add(&r); + + r.h = Request::HREQUEST_REFERENCE_YZ; + SK.request.Add(&r); + + r.h = Request::HREQUEST_REFERENCE_ZX; + SK.request.Add(&r); + + CreateDefaultDrawingGroup(); +} + +const SolveSpaceUI::SaveTable SolveSpaceUI::SAVED[] = { + { 'g', "Group.h.v", 'x', &(SS.sv.g.h.v) }, + { 'g', "Group.type", 'd', &(SS.sv.g.type) }, + { 'g', "Group.order", 'd', &(SS.sv.g.order) }, + { 'g', "Group.name", 'S', &(SS.sv.g.name) }, + { 'g', "Group.activeWorkplane.v", 'x', &(SS.sv.g.activeWorkplane.v) }, + { 'g', "Group.opA.v", 'x', &(SS.sv.g.opA.v) }, + { 'g', "Group.opB.v", 'x', &(SS.sv.g.opB.v) }, + { 'g', "Group.valA", 'f', &(SS.sv.g.valA) }, + { 'g', "Group.valB", 'f', &(SS.sv.g.valB) }, + { 'g', "Group.valC", 'f', &(SS.sv.g.valB) }, + { 'g', "Group.color", 'c', &(SS.sv.g.color) }, + { 'g', "Group.subtype", 'd', &(SS.sv.g.subtype) }, + { 'g', "Group.skipFirst", 'b', &(SS.sv.g.skipFirst) }, + { 'g', "Group.meshCombine", 'd', &(SS.sv.g.meshCombine) }, + { 'g', "Group.forceToMesh", 'd', &(SS.sv.g.forceToMesh) }, + { 'g', "Group.predef.q.w", 'f', &(SS.sv.g.predef.q.w) }, + { 'g', "Group.predef.q.vx", 'f', &(SS.sv.g.predef.q.vx) }, + { 'g', "Group.predef.q.vy", 'f', &(SS.sv.g.predef.q.vy) }, + { 'g', "Group.predef.q.vz", 'f', &(SS.sv.g.predef.q.vz) }, + { 'g', "Group.predef.origin.v", 'x', &(SS.sv.g.predef.origin.v) }, + { 'g', "Group.predef.entityB.v", 'x', &(SS.sv.g.predef.entityB.v) }, + { 'g', "Group.predef.entityC.v", 'x', &(SS.sv.g.predef.entityC.v) }, + { 'g', "Group.predef.swapUV", 'b', &(SS.sv.g.predef.swapUV) }, + { 'g', "Group.predef.negateU", 'b', &(SS.sv.g.predef.negateU) }, + { 'g', "Group.predef.negateV", 'b', &(SS.sv.g.predef.negateV) }, + { 'g', "Group.visible", 'b', &(SS.sv.g.visible) }, + { 'g', "Group.suppress", 'b', &(SS.sv.g.suppress) }, + { 'g', "Group.relaxConstraints", 'b', &(SS.sv.g.relaxConstraints) }, + { 'g', "Group.allowRedundant", 'b', &(SS.sv.g.allowRedundant) }, + { 'g', "Group.allDimsReference", 'b', &(SS.sv.g.allDimsReference) }, + { 'g', "Group.scale", 'f', &(SS.sv.g.scale) }, + { 'g', "Group.remap", 'M', &(SS.sv.g.remap) }, + { 'g', "Group.impFile", 'i', NULL }, + { 'g', "Group.impFileRel", 'P', &(SS.sv.g.linkFile) }, + + { 'p', "Param.h.v.", 'x', &(SS.sv.p.h.v) }, + { 'p', "Param.val", 'f', &(SS.sv.p.val) }, + + { 'r', "Request.h.v", 'x', &(SS.sv.r.h.v) }, + { 'r', "Request.type", 'd', &(SS.sv.r.type) }, + { 'r', "Request.extraPoints", 'd', &(SS.sv.r.extraPoints) }, + { 'r', "Request.workplane.v", 'x', &(SS.sv.r.workplane.v) }, + { 'r', "Request.group.v", 'x', &(SS.sv.r.group.v) }, + { 'r', "Request.construction", 'b', &(SS.sv.r.construction) }, + { 'r', "Request.style", 'x', &(SS.sv.r.style) }, + { 'r', "Request.str", 'S', &(SS.sv.r.str) }, + { 'r', "Request.font", 'S', &(SS.sv.r.font) }, + { 'r', "Request.file", 'P', &(SS.sv.r.file) }, + { 'r', "Request.aspectRatio", 'f', &(SS.sv.r.aspectRatio) }, + + { 'e', "Entity.h.v", 'x', &(SS.sv.e.h.v) }, + { 'e', "Entity.type", 'd', &(SS.sv.e.type) }, + { 'e', "Entity.construction", 'b', &(SS.sv.e.construction) }, + { 'e', "Entity.style", 'x', &(SS.sv.e.style) }, + { 'e', "Entity.str", 'S', &(SS.sv.e.str) }, + { 'e', "Entity.font", 'S', &(SS.sv.e.font) }, + { 'e', "Entity.file", 'P', &(SS.sv.e.file) }, + { 'e', "Entity.point[0].v", 'x', &(SS.sv.e.point[0].v) }, + { 'e', "Entity.point[1].v", 'x', &(SS.sv.e.point[1].v) }, + { 'e', "Entity.point[2].v", 'x', &(SS.sv.e.point[2].v) }, + { 'e', "Entity.point[3].v", 'x', &(SS.sv.e.point[3].v) }, + { 'e', "Entity.point[4].v", 'x', &(SS.sv.e.point[4].v) }, + { 'e', "Entity.point[5].v", 'x', &(SS.sv.e.point[5].v) }, + { 'e', "Entity.point[6].v", 'x', &(SS.sv.e.point[6].v) }, + { 'e', "Entity.point[7].v", 'x', &(SS.sv.e.point[7].v) }, + { 'e', "Entity.point[8].v", 'x', &(SS.sv.e.point[8].v) }, + { 'e', "Entity.point[9].v", 'x', &(SS.sv.e.point[9].v) }, + { 'e', "Entity.point[10].v", 'x', &(SS.sv.e.point[10].v) }, + { 'e', "Entity.point[11].v", 'x', &(SS.sv.e.point[11].v) }, + { 'e', "Entity.extraPoints", 'd', &(SS.sv.e.extraPoints) }, + { 'e', "Entity.normal.v", 'x', &(SS.sv.e.normal.v) }, + { 'e', "Entity.distance.v", 'x', &(SS.sv.e.distance.v) }, + { 'e', "Entity.workplane.v", 'x', &(SS.sv.e.workplane.v) }, + { 'e', "Entity.actPoint.x", 'f', &(SS.sv.e.actPoint.x) }, + { 'e', "Entity.actPoint.y", 'f', &(SS.sv.e.actPoint.y) }, + { 'e', "Entity.actPoint.z", 'f', &(SS.sv.e.actPoint.z) }, + { 'e', "Entity.actNormal.w", 'f', &(SS.sv.e.actNormal.w) }, + { 'e', "Entity.actNormal.vx", 'f', &(SS.sv.e.actNormal.vx) }, + { 'e', "Entity.actNormal.vy", 'f', &(SS.sv.e.actNormal.vy) }, + { 'e', "Entity.actNormal.vz", 'f', &(SS.sv.e.actNormal.vz) }, + { 'e', "Entity.actDistance", 'f', &(SS.sv.e.actDistance) }, + { 'e', "Entity.actVisible", 'b', &(SS.sv.e.actVisible), }, + + + { 'c', "Constraint.h.v", 'x', &(SS.sv.c.h.v) }, + { 'c', "Constraint.type", 'd', &(SS.sv.c.type) }, + { 'c', "Constraint.group.v", 'x', &(SS.sv.c.group.v) }, + { 'c', "Constraint.workplane.v", 'x', &(SS.sv.c.workplane.v) }, + { 'c', "Constraint.valA", 'f', &(SS.sv.c.valA) }, + { 'c', "Constraint.valP.v", 'x', &(SS.sv.c.valP.v) }, + { 'c', "Constraint.ptA.v", 'x', &(SS.sv.c.ptA.v) }, + { 'c', "Constraint.ptB.v", 'x', &(SS.sv.c.ptB.v) }, + { 'c', "Constraint.entityA.v", 'x', &(SS.sv.c.entityA.v) }, + { 'c', "Constraint.entityB.v", 'x', &(SS.sv.c.entityB.v) }, + { 'c', "Constraint.entityC.v", 'x', &(SS.sv.c.entityC.v) }, + { 'c', "Constraint.entityD.v", 'x', &(SS.sv.c.entityD.v) }, + { 'c', "Constraint.other", 'b', &(SS.sv.c.other) }, + { 'c', "Constraint.other2", 'b', &(SS.sv.c.other2) }, + { 'c', "Constraint.reference", 'b', &(SS.sv.c.reference) }, + { 'c', "Constraint.comment", 'S', &(SS.sv.c.comment) }, + { 'c', "Constraint.disp.offset.x", 'f', &(SS.sv.c.disp.offset.x) }, + { 'c', "Constraint.disp.offset.y", 'f', &(SS.sv.c.disp.offset.y) }, + { 'c', "Constraint.disp.offset.z", 'f', &(SS.sv.c.disp.offset.z) }, + { 'c', "Constraint.disp.style", 'x', &(SS.sv.c.disp.style) }, + + { 's', "Style.h.v", 'x', &(SS.sv.s.h.v) }, + { 's', "Style.name", 'S', &(SS.sv.s.name) }, + { 's', "Style.width", 'f', &(SS.sv.s.width) }, + { 's', "Style.widthAs", 'd', &(SS.sv.s.widthAs) }, + { 's', "Style.textHeight", 'f', &(SS.sv.s.textHeight) }, + { 's', "Style.textHeightAs", 'd', &(SS.sv.s.textHeightAs) }, + { 's', "Style.textAngle", 'f', &(SS.sv.s.textAngle) }, + { 's', "Style.textOrigin", 'x', &(SS.sv.s.textOrigin) }, + { 's', "Style.color", 'c', &(SS.sv.s.color) }, + { 's', "Style.fillColor", 'c', &(SS.sv.s.fillColor) }, + { 's', "Style.filled", 'b', &(SS.sv.s.filled) }, + { 's', "Style.visible", 'b', &(SS.sv.s.visible) }, + { 's', "Style.exportable", 'b', &(SS.sv.s.exportable) }, + { 's', "Style.stippleType", 'd', &(SS.sv.s.stippleType) }, + { 's', "Style.stippleScale", 'f', &(SS.sv.s.stippleScale) }, + + { 0, NULL, 0, NULL } +}; + +struct SAVEDptr { + EntityMap &M() { return *((EntityMap *)this); } + std::string &S() { return *((std::string *)this); } + Platform::Path &P() { return *((Platform::Path *)this); } + bool &b() { return *((bool *)this); } + RgbaColor &c() { return *((RgbaColor *)this); } + int &d() { return *((int *)this); } + double &f() { return *((double *)this); } + uint32_t &x() { return *((uint32_t *)this); } +}; + +void SolveSpaceUI::SaveUsingTable(const Platform::Path &filename, int type) { + int i; + for(i = 0; SAVED[i].type != 0; i++) { + if(SAVED[i].type != type) continue; + + int fmt = SAVED[i].fmt; + SAVEDptr *p = (SAVEDptr *)SAVED[i].ptr; + // Any items that aren't specified are assumed to be zero + if(fmt == 'S' && p->S().empty()) continue; + if(fmt == 'P' && p->P().IsEmpty()) continue; + if(fmt == 'd' && p->d() == 0) continue; + if(fmt == 'f' && EXACT(p->f() == 0.0)) continue; + if(fmt == 'x' && p->x() == 0) continue; + if(fmt == 'i') continue; + + fprintf(fh, "%s=", SAVED[i].desc); + switch(fmt) { + case 'S': fprintf(fh, "%s", p->S().c_str()); break; + case 'b': fprintf(fh, "%d", p->b() ? 1 : 0); break; + case 'c': fprintf(fh, "%08x", p->c().ToPackedInt()); break; + case 'd': fprintf(fh, "%d", p->d()); break; + case 'f': fprintf(fh, "%.20f", p->f()); break; + case 'x': fprintf(fh, "%08x", p->x()); break; + + case 'P': { + if(!p->P().IsEmpty()) { + Platform::Path relativePath = p->P().Expand(/*fromCurrentDirectory=*/true).RelativeTo(filename.Expand(/*fromCurrentDirectory=*/true).Parent()); + ssassert(!relativePath.IsEmpty(), "Cannot relativize path"); + fprintf(fh, "%s", relativePath.ToPortable().c_str()); + } + break; + } + + case 'M': { + fprintf(fh, "{\n"); + // Sort the mapping, since EntityMap is not deterministic. + std::vector> sorted(p->M().begin(), p->M().end()); + std::sort(sorted.begin(), sorted.end(), + [](std::pair &a, std::pair &b) { + return a.second.v < b.second.v; + }); + for(const auto &it : sorted) { + fprintf(fh, " %d %08x %d\n", + it.second.v, it.first.input.v, it.first.copyNumber); + } + fprintf(fh, "}"); + break; + } + + case 'i': break; + + default: ssassert(false, "Unexpected value format"); + } + fprintf(fh, "\n"); + } +} + +bool SolveSpaceUI::SaveToFile(const Platform::Path &filename) { + // Make sure all the entities are regenerated up to date, since they will be exported. + SS.ScheduleShowTW(); + SS.GenerateAll(SolveSpaceUI::Generate::ALL); + + for(Group &g : SK.group) { + if(g.type != Group::Type::LINKED) continue; + + // Expand for "filename" below is needed on Linux when the file was opened with a relative + // path on the command line. dialog->RunModal() in SolveSpaceUI::GetFilenameAndSave will + // convert the file name to full path on Windows but not on GTK. + if(g.linkFile.Expand(/*fromCurrentDirectory=*/true) + .RelativeTo(filename.Expand(/*fromCurrentDirectory=*/true)) + .IsEmpty()) { + Error("This sketch links the sketch '%s'; it can only be saved " + "on the same volume.", g.linkFile.raw.c_str()); + return false; + } + } + + fh = OpenFile(filename, "wb"); + if(!fh) { + Error("Couldn't write to file '%s'", filename.raw.c_str()); + return false; + } + + fprintf(fh, "%s\n\n\n", VERSION_STRING); + + int i, j; + for(auto &g : SK.group) { + sv.g = g; + SaveUsingTable(filename, 'g'); + fprintf(fh, "AddGroup\n\n"); + } + + for(auto &p : SK.param) { + sv.p = p; + SaveUsingTable(filename, 'p'); + fprintf(fh, "AddParam\n\n"); + } + + for(auto &r : SK.request) { + sv.r = r; + SaveUsingTable(filename, 'r'); + fprintf(fh, "AddRequest\n\n"); + } + + for(auto &e : SK.entity) { + e.CalculateNumerical(/*forExport=*/true); + sv.e = e; + SaveUsingTable(filename, 'e'); + fprintf(fh, "AddEntity\n\n"); + } + + for(auto &c : SK.constraint) { + sv.c = c; + SaveUsingTable(filename, 'c'); + fprintf(fh, "AddConstraint\n\n"); + } + + for(auto &s : SK.style) { + sv.s = s; + if(sv.s.h.v >= Style::FIRST_CUSTOM) { + SaveUsingTable(filename, 's'); + fprintf(fh, "AddStyle\n\n"); + } + } + + // A group will have either a mesh or a shell, but not both; but the code + // to print either of those just does nothing if the mesh/shell is empty. + + Group *g = SK.GetGroup(*SK.groupOrder.Last()); + SMesh *m = &g->runningMesh; + for(i = 0; i < m->l.n; i++) { + STriangle *tr = &(m->l[i]); + fprintf(fh, "Triangle %08x %08x " + "%.20f %.20f %.20f %.20f %.20f %.20f %.20f %.20f %.20f\n", + tr->meta.face, tr->meta.color.ToPackedInt(), + CO(tr->a), CO(tr->b), CO(tr->c)); + } + + SShell *s = &g->runningShell; + for(SSurface &srf : s->surface) { + fprintf(fh, "Surface %08x %08x %08x %d %d\n", + srf.h.v, srf.color.ToPackedInt(), srf.face, srf.degm, srf.degn); + for(i = 0; i <= srf.degm; i++) { + for(j = 0; j <= srf.degn; j++) { + fprintf(fh, "SCtrl %d %d %.20f %.20f %.20f Weight %20.20f\n", + i, j, CO(srf.ctrl[i][j]), srf.weight[i][j]); + } + } + + STrimBy *stb; + for(stb = srf.trim.First(); stb; stb = srf.trim.NextAfter(stb)) { + fprintf(fh, "TrimBy %08x %d %.20f %.20f %.20f %.20f %.20f %.20f\n", + stb->curve.v, stb->backwards ? 1 : 0, + CO(stb->start), CO(stb->finish)); + } + + fprintf(fh, "AddSurface\n"); + } + for(SCurve &sc : s->curve) { + fprintf(fh, "Curve %08x %d %d %08x %08x\n", + sc.h.v, + sc.isExact ? 1 : 0, sc.exact.deg, + sc.surfA.v, sc.surfB.v); + + if(sc.isExact) { + for(i = 0; i <= sc.exact.deg; i++) { + fprintf(fh, "CCtrl %d %.20f %.20f %.20f Weight %.20f\n", + i, CO(sc.exact.ctrl[i]), sc.exact.weight[i]); + } + } + SCurvePt *scpt; + for(scpt = sc.pts.First(); scpt; scpt = sc.pts.NextAfter(scpt)) { + fprintf(fh, "CurvePt %d %.20f %.20f %.20f\n", + scpt->vertex ? 1 : 0, CO(scpt->p)); + } + + fprintf(fh, "AddCurve\n"); + } + + fclose(fh); + + return true; +} + +void SolveSpaceUI::LoadUsingTable(const Platform::Path &filename, char *key, char *val) { + int i; + for(i = 0; SAVED[i].type != 0; i++) { + if(strcmp(SAVED[i].desc, key)==0) { + SAVEDptr *p = (SAVEDptr *)SAVED[i].ptr; + unsigned int u = 0; + switch(SAVED[i].fmt) { + case 'S': p->S() = val; break; + case 'b': p->b() = (atoi(val) != 0); break; + case 'd': p->d() = atoi(val); break; + case 'f': p->f() = atof(val); break; + case 'x': sscanf(val, "%x", &u); p->x()= u; break; + + case 'P': { + Platform::Path path = Platform::Path::FromPortable(val); + if(!path.IsEmpty()) { + p->P() = filename.Parent().Join(path).Expand(); + } + break; + } + + case 'c': + sscanf(val, "%x", &u); + p->c() = RgbaColor::FromPackedInt(u); + break; + + case 'M': { + p->M().clear(); + for(;;) { + EntityKey ek; + EntityId ei; + char line2[1024]; + if (fgets(line2, (int)sizeof(line2), fh) == NULL) + break; + if(sscanf(line2, "%d %x %d", &(ei.v), &(ek.input.v), + &(ek.copyNumber)) == 3) { + if(ei.v == Entity::NO_ENTITY.v) { + // Commit bd84bc1a mistakenly introduced code that would remap + // some entities to NO_ENTITY. This was fixed in commit bd84bc1a, + // but files created meanwhile are corrupt, and can cause crashes. + // + // To fix this, we skip any such remaps when loading; they will be + // recreated on the next regeneration. Any resulting orphans will + // be pruned in the usual way, recovering to a well-defined state. + continue; + } + p->M().insert({ ek, ei }); + } else { + break; + } + } + break; + } + + case 'i': break; + + default: ssassert(false, "Unexpected value format"); + } + break; + } + } + if(SAVED[i].type == 0) { + fileLoadError = true; + } +} + +bool SolveSpaceUI::LoadFromFile(const Platform::Path &filename, bool canCancel) { + bool fileIsEmpty = true; + allConsistent = false; + fileLoadError = false; + + fh = OpenFile(filename, "rb"); + if(!fh) { + Error("Couldn't read from file '%s'", filename.raw.c_str()); + return false; + } + + ClearExisting(); + + sv = {}; + sv.g.scale = 1; // default is 1, not 0; so legacy files need this + Style::FillDefaultStyle(&sv.s); + + char line[1024]; + while(fgets(line, (int)sizeof(line), fh)) { + fileIsEmpty = false; + + char *s = strchr(line, '\n'); + if(s) *s = '\0'; + // We should never get files with \r characters in them, but mailers + // will sometimes mangle attachments. + s = strchr(line, '\r'); + if(s) *s = '\0'; + + if(*line == '\0') continue; + + char *e = strchr(line, '='); + if(e) { + *e = '\0'; + char *key = line, *val = e+1; + LoadUsingTable(filename, key, val); + } else if(strcmp(line, "AddGroup")==0) { + // legacy files have a spurious dependency between linked groups + // and their parent groups, remove + if(sv.g.type == Group::Type::LINKED) + sv.g.opA.v = 0; + + SK.group.Add(&(sv.g)); + sv.g = {}; + sv.g.scale = 1; // default is 1, not 0; so legacy files need this + } else if(strcmp(line, "AddParam")==0) { + // params are regenerated, but we want to preload the values + // for initial guesses + SK.param.Add(&(sv.p)); + sv.p = {}; + } else if(strcmp(line, "AddEntity")==0) { + // entities are regenerated + } else if(strcmp(line, "AddRequest")==0) { + SK.request.Add(&(sv.r)); + sv.r = {}; + } else if(strcmp(line, "AddConstraint")==0) { + SK.constraint.Add(&(sv.c)); + sv.c = {}; + } else if(strcmp(line, "AddStyle")==0) { + SK.style.Add(&(sv.s)); + sv.s = {}; + Style::FillDefaultStyle(&sv.s); + } else if(strcmp(line, VERSION_STRING)==0) { + // do nothing, version string + } else if(StrStartsWith(line, "Triangle ") || + StrStartsWith(line, "Surface ") || + StrStartsWith(line, "SCtrl ") || + StrStartsWith(line, "TrimBy ") || + StrStartsWith(line, "Curve ") || + StrStartsWith(line, "CCtrl ") || + StrStartsWith(line, "CurvePt ") || + strcmp(line, "AddSurface")==0 || + strcmp(line, "AddCurve")==0) + { + // ignore the mesh or shell, since we regenerate that + } else { + fileLoadError = true; + } + } + + fclose(fh); + + if(fileIsEmpty) { + Error(_("The file is empty. It may be corrupt.")); + NewFile(); + } + + if(fileLoadError) { + Error(_("Unrecognized data in file. This file may be corrupt, or " + "from a newer version of the program.")); + // At least leave the program in a non-crashing state. + if(SK.group.IsEmpty()) { + NewFile(); + } + } + if(!ReloadAllLinked(filename, canCancel)) { + return false; + } + UpgradeLegacyData(); + + return true; +} + +void SolveSpaceUI::UpgradeLegacyData() { + for(Request &r : SK.request) { + switch(r.type) { + // TTF text requests saved in versions prior to 3.0 only have two + // reference points (origin and origin plus v); version 3.0 adds two + // more points, and if we don't do anything, then they will appear + // at workplane origin, and the solver will mess up the sketch if + // it is not fully constrained. + case Request::Type::TTF_TEXT: { + EntityList entity = {}; + ParamList param = {}; + r.Generate(&entity, ¶m); + + // If we didn't load all of the entities and params that this + // request would generate, then add them now, so that we can + // force them to their appropriate positions. + for(Param &p : param) { + if(SK.param.FindByIdNoOops(p.h) != NULL) continue; + SK.param.Add(&p); + } + bool allPointsExist = true; + for(Entity &e : entity) { + if(SK.entity.FindByIdNoOops(e.h) != NULL) continue; + SK.entity.Add(&e); + allPointsExist = false; + } + + if(!allPointsExist) { + Entity *text = entity.FindById(r.h.entity(0)); + Entity *b = entity.FindById(text->point[2]); + Entity *c = entity.FindById(text->point[3]); + ExprVector bex, cex; + text->RectGetPointsExprs(&bex, &cex); + b->PointForceParamTo(bex.Eval()); + c->PointForceParamTo(cex.Eval()); + } + entity.Clear(); + param.Clear(); + break; + } + + default: + break; + } + } + + // Constraints saved in versions prior to 3.0 never had any params; + // version 3.0 introduced params to constraints to avoid the hairy ball problem, + // so force them where they belong. + ParamList oldParam = {}; + SK.param.DeepCopyInto(&oldParam); + SS.GenerateAll(SolveSpaceUI::Generate::REGEN); + + auto AllParamsExistFor = [&](Constraint &c) { + ParamList param = {}; + c.Generate(¶m); + bool allParamsExist = true; + for(Param &p : param) { + if(oldParam.FindByIdNoOops(p.h) != NULL) continue; + allParamsExist = false; + break; + } + param.Clear(); + return allParamsExist; + }; + + for(Constraint &c : SK.constraint) { + switch(c.type) { + case Constraint::Type::PT_ON_LINE: { + if(AllParamsExistFor(c)) continue; + + EntityBase *eln = SK.GetEntity(c.entityA); + EntityBase *ea = SK.GetEntity(eln->point[0]); + EntityBase *eb = SK.GetEntity(eln->point[1]); + EntityBase *ep = SK.GetEntity(c.ptA); + + ExprVector exp = ep->PointGetExprsInWorkplane(c.workplane); + ExprVector exa = ea->PointGetExprsInWorkplane(c.workplane); + ExprVector exb = eb->PointGetExprsInWorkplane(c.workplane); + ExprVector exba = exb.Minus(exa); + Param *p = SK.GetParam(c.h.param(0)); + p->val = exba.Dot(exp.Minus(exa))->Eval() / exba.Dot(exba)->Eval(); + break; + } + + case Constraint::Type::CUBIC_LINE_TANGENT: { + if(AllParamsExistFor(c)) continue; + + EntityBase *cubic = SK.GetEntity(c.entityA); + EntityBase *line = SK.GetEntity(c.entityB); + + ExprVector a; + if(c.other) { + a = cubic->CubicGetFinishTangentExprs(); + } else { + a = cubic->CubicGetStartTangentExprs(); + } + + ExprVector b = line->VectorGetExprs(); + + Param *param = SK.GetParam(c.h.param(0)); + param->val = a.Dot(b)->Eval() / b.Dot(b)->Eval(); + break; + } + + case Constraint::Type::SAME_ORIENTATION: { + if(AllParamsExistFor(c)) continue; + + EntityBase *an = SK.GetEntity(c.entityA); + EntityBase *bn = SK.GetEntity(c.entityB); + + ExprVector a = an->NormalExprsN(); + ExprVector b = bn->NormalExprsN(); + + Param *param = SK.GetParam(c.h.param(0)); + param->val = a.Dot(b)->Eval() / b.Dot(b)->Eval(); + break; + } + + case Constraint::Type::PARALLEL: { + if(AllParamsExistFor(c)) continue; + + EntityBase *ea = SK.GetEntity(c.entityA), + *eb = SK.GetEntity(c.entityB); + ExprVector a = ea->VectorGetExprsInWorkplane(c.workplane); + ExprVector b = eb->VectorGetExprsInWorkplane(c.workplane); + + Param *param = SK.GetParam(c.h.param(0)); + param->val = a.Dot(b)->Eval() / b.Dot(b)->Eval(); + break; + } + + default: + break; + } + } + oldParam.Clear(); +} + +bool SolveSpaceUI::LoadEntitiesFromFile(const Platform::Path &filename, EntityList *le, + SMesh *m, SShell *sh) +{ + if(strcmp(filename.Extension().c_str(), "emn")==0) { + return LinkIDF(filename, le, m, sh); + } else if(strcmp(filename.Extension().c_str(), "EMN")==0) { + return LinkIDF(filename, le, m, sh); + } else if(strcmp(filename.Extension().c_str(), "stl")==0) { + return LinkStl(filename, le, m, sh); + } else if(strcmp(filename.Extension().c_str(), "STL")==0) { + return LinkStl(filename, le, m, sh); + } else { + return LoadEntitiesFromSlvs(filename, le, m, sh); + } +} + +bool SolveSpaceUI::LoadEntitiesFromSlvs(const Platform::Path &filename, EntityList *le, + SMesh *m, SShell *sh) +{ + SSurface srf = {}; + SCurve crv = {}; + + fh = OpenFile(filename, "rb"); + if(!fh) return false; + + le->Clear(); + sv = {}; + + char line[1024]; + while(fgets(line, (int)sizeof(line), fh)) { + char *s = strchr(line, '\n'); + if(s) *s = '\0'; + // We should never get files with \r characters in them, but mailers + // will sometimes mangle attachments. + s = strchr(line, '\r'); + if(s) *s = '\0'; + + if(*line == '\0') continue; + + char *e = strchr(line, '='); + if(e) { + *e = '\0'; + char *key = line, *val = e+1; + LoadUsingTable(filename, key, val); + } else if(strcmp(line, "AddGroup")==0) { + // These get allocated whether we want them or not. + sv.g.remap.clear(); + } else if(strcmp(line, "AddParam")==0) { + + } else if(strcmp(line, "AddEntity")==0) { + le->Add(&(sv.e)); + sv.e = {}; + } else if(strcmp(line, "AddRequest")==0) { + + } else if(strcmp(line, "AddConstraint")==0) { + + } else if(strcmp(line, "AddStyle")==0) { + // Linked file contains a style that we don't have yet, + // so import it. + if (SK.style.FindByIdNoOops(sv.s.h) == nullptr) { + SK.style.Add(&(sv.s)); + } + sv.s = {}; + Style::FillDefaultStyle(&sv.s); + } else if(strcmp(line, VERSION_STRING)==0) { + + } else if(StrStartsWith(line, "Triangle ")) { + STriangle tr = {}; + unsigned int rgba = 0; + if(sscanf(line, "Triangle %x %x " + "%lf %lf %lf %lf %lf %lf %lf %lf %lf", + &(tr.meta.face), &rgba, + &(tr.a.x), &(tr.a.y), &(tr.a.z), + &(tr.b.x), &(tr.b.y), &(tr.b.z), + &(tr.c.x), &(tr.c.y), &(tr.c.z)) != 11) { + ssassert(false, "Unexpected Triangle format"); + } + tr.meta.color = RgbaColor::FromPackedInt((uint32_t)rgba); + m->AddTriangle(&tr); + } else if(StrStartsWith(line, "Surface ")) { + unsigned int rgba = 0; + if(sscanf(line, "Surface %x %x %x %d %d", + &(srf.h.v), &rgba, &(srf.face), + &(srf.degm), &(srf.degn)) != 5) { + ssassert(false, "Unexpected Surface format"); + } + srf.color = RgbaColor::FromPackedInt((uint32_t)rgba); + } else if(StrStartsWith(line, "SCtrl ")) { + int i, j; + Vector c; + double w; + if(sscanf(line, "SCtrl %d %d %lf %lf %lf Weight %lf", + &i, &j, &(c.x), &(c.y), &(c.z), &w) != 6) + { + ssassert(false, "Unexpected SCtrl format"); + } + srf.ctrl[i][j] = c; + srf.weight[i][j] = w; + } else if(StrStartsWith(line, "TrimBy ")) { + STrimBy stb = {}; + int backwards; + if(sscanf(line, "TrimBy %x %d %lf %lf %lf %lf %lf %lf", + &(stb.curve.v), &backwards, + &(stb.start.x), &(stb.start.y), &(stb.start.z), + &(stb.finish.x), &(stb.finish.y), &(stb.finish.z)) != 8) + { + ssassert(false, "Unexpected TrimBy format"); + } + stb.backwards = (backwards != 0); + srf.trim.Add(&stb); + } else if(strcmp(line, "AddSurface")==0) { + sh->surface.Add(&srf); + srf = {}; + } else if(StrStartsWith(line, "Curve ")) { + int isExact; + if(sscanf(line, "Curve %x %d %d %x %x", + &(crv.h.v), + &(isExact), + &(crv.exact.deg), + &(crv.surfA.v), &(crv.surfB.v)) != 5) + { + ssassert(false, "Unexpected Curve format"); + } + crv.isExact = (isExact != 0); + } else if(StrStartsWith(line, "CCtrl ")) { + int i; + Vector c; + double w; + if(sscanf(line, "CCtrl %d %lf %lf %lf Weight %lf", + &i, &(c.x), &(c.y), &(c.z), &w) != 5) + { + ssassert(false, "Unexpected CCtrl format"); + } + crv.exact.ctrl[i] = c; + crv.exact.weight[i] = w; + } else if(StrStartsWith(line, "CurvePt ")) { + SCurvePt scpt; + int vertex; + if(sscanf(line, "CurvePt %d %lf %lf %lf", + &vertex, + &(scpt.p.x), &(scpt.p.y), &(scpt.p.z)) != 4) + { + ssassert(false, "Unexpected CurvePt format"); + } + scpt.vertex = (vertex != 0); + crv.pts.Add(&scpt); + } else if(strcmp(line, "AddCurve")==0) { + sh->curve.Add(&crv); + crv = {}; + } else ssassert(false, "Unexpected operation"); + } + + fclose(fh); + return true; +} + +static Platform::MessageDialog::Response LocateImportedFile(const Platform::Path &filename, + bool canCancel) { + Platform::MessageDialogRef dialog = CreateMessageDialog(SS.GW.window); + + using Platform::MessageDialog; + dialog->SetType(MessageDialog::Type::QUESTION); + dialog->SetTitle(C_("title", "Missing File")); + dialog->SetMessage(ssprintf(C_("dialog", "The linked file “%s” is not present."), + filename.raw.c_str())); + dialog->SetDescription(C_("dialog", "Do you want to locate it manually?\n\n" + "If you decline, any geometry that depends on " + "the missing file will be permanently removed.")); + dialog->AddButton(C_("button", "&Yes"), MessageDialog::Response::YES, + /*isDefault=*/true); + dialog->AddButton(C_("button", "&No"), MessageDialog::Response::NO); + if(canCancel) { + dialog->AddButton(C_("button", "&Cancel"), MessageDialog::Response::CANCEL); + } + + // FIXME(async): asyncify this call + return dialog->RunModal(); +} + +bool SolveSpaceUI::ReloadAllLinked(const Platform::Path &saveFile, bool canCancel) { + Platform::SettingsRef settings = Platform::GetSettings(); + + std::map linkMap; + + allConsistent = false; + + for(Group &g : SK.group) { + if(g.type != Group::Type::LINKED) continue; + + g.impEntity.Clear(); + g.impMesh.Clear(); + g.impShell.Clear(); + + // If we prompted for this specific file before, don't ask again. + if(linkMap.count(g.linkFile)) { + g.linkFile = linkMap[g.linkFile]; + } + +try_again: + if(LoadEntitiesFromFile(g.linkFile, &g.impEntity, &g.impMesh, &g.impShell)) { + // We loaded the data, good. Now import its dependencies as well. + for(Entity &e : g.impEntity) { + if(e.type != Entity::Type::IMAGE) continue; + if(!ReloadLinkedImage(g.linkFile, &e.file, canCancel)) { + return false; + } + } + if(g.IsTriangleMeshAssembly()) + g.forceToMesh = true; + } else if(linkMap.count(g.linkFile) == 0) { + dbp("Missing file for group: %s", g.name.c_str()); + // The file was moved; prompt the user for its new location. + const auto linkFileRelative = g.linkFile.RelativeTo(saveFile); + switch(LocateImportedFile(linkFileRelative, canCancel)) { + case Platform::MessageDialog::Response::YES: { + Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window); + dialog->AddFilters(Platform::SolveSpaceLinkFileFilters); + dialog->ThawChoices(settings, "LinkSketch"); + dialog->SuggestFilename(linkFileRelative); + if(dialog->RunModal()) { + dialog->FreezeChoices(settings, "LinkSketch"); + linkMap[g.linkFile] = dialog->GetFilename(); + g.linkFile = dialog->GetFilename(); + goto try_again; + } else { + if(canCancel) return false; + break; + } + } + + case Platform::MessageDialog::Response::NO: + linkMap[g.linkFile].Clear(); + // Geometry will be pruned by GenerateAll(). + break; + + case Platform::MessageDialog::Response::CANCEL: + return false; + + default: + ssassert(false, "Unexpected dialog response"); + } + } else { + // User was already asked to and refused to locate a missing linked file. + } + } + + for(Request &r : SK.request) { + if(r.type != Request::Type::IMAGE) continue; + + if(!ReloadLinkedImage(saveFile, &r.file, canCancel)) { + return false; + } + } + + return true; +} + +bool SolveSpaceUI::ReloadLinkedImage(const Platform::Path &saveFile, + Platform::Path *filename, bool canCancel) { + Platform::SettingsRef settings = Platform::GetSettings(); + + std::shared_ptr pixmap; + bool promptOpenFile = false; + if(filename->IsEmpty()) { + // We're prompting the user for a new image. + promptOpenFile = true; + } else { + auto image = SS.images.find(*filename); + if(image != SS.images.end()) return true; + + pixmap = Pixmap::ReadPng(*filename); + if(pixmap == NULL) { + // The file was moved; prompt the user for its new location. + switch(LocateImportedFile(filename->RelativeTo(saveFile), canCancel)) { + case Platform::MessageDialog::Response::YES: + promptOpenFile = true; + break; + + case Platform::MessageDialog::Response::NO: + // We don't know where the file is, record it as absent. + break; + + case Platform::MessageDialog::Response::CANCEL: + return false; + + default: + ssassert(false, "Unexpected dialog response"); + } + } + } + + if(promptOpenFile) { + Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window); + dialog->AddFilters(Platform::RasterFileFilters); + dialog->ThawChoices(settings, "LinkImage"); + dialog->SuggestFilename(filename->RelativeTo(saveFile)); + if(dialog->RunModal()) { + dialog->FreezeChoices(settings, "LinkImage"); + *filename = dialog->GetFilename(); + pixmap = Pixmap::ReadPng(*filename); + if(pixmap == NULL) { + Error("The image '%s' is corrupted.", filename->raw.c_str()); + } + // We know where the file is now, good. + } else if(canCancel) { + return false; + } + } + + // We loaded the data, good. + SS.images[*filename] = pixmap; + return true; +} + +} // namespace SolveSpace diff --git a/src/generate.cpp b/src/generate.cpp new file mode 100644 index 0000000..4d38a52 --- /dev/null +++ b/src/generate.cpp @@ -0,0 +1,594 @@ +//----------------------------------------------------------------------------- +// Generate our model based on its parametric description, by solving each +// sketch, generating surfaces from the resulting entities, performing any +// requested surface operations (e.g. Booleans) with our model so far, and +// then repeating this process for each subsequent group. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +void SolveSpaceUI::MarkGroupDirtyByEntity(hEntity he) { + Entity *e = SK.GetEntity(he); + MarkGroupDirty(e->group); +} + +void SolveSpaceUI::MarkGroupDirty(hGroup hg, bool onlyThis) { + bool go = false; + for(auto const &gh : SK.groupOrder) { + Group *g = SK.GetGroup(gh); + if(g->h == hg) { + go = true; + } + if(go) { + g->clean = false; + if(onlyThis) break; + } + } + unsaved = true; + ScheduleGenerateAll(); +} + +bool SolveSpaceUI::PruneOrphans() { + const int requests = SK.request.n; + for(Request &r : SK.request) { + if(!GroupExists(r.group)) + r.tag = 1; + } + SK.request.RemoveTagged(); + deleted.requests += requests - SK.request.n; + + const int constraints = SK.constraint.n; + for(Constraint &c : SK.constraint) { + if(!GroupExists(c.group)) + c.tag = 1; + } + SK.constraint.RemoveTagged(); + deleted.constraints += constraints - SK.constraint.n; + deleted.nonTrivialConstraints += constraints - SK.constraint.n; + + return (requests > SK.request.n) || (constraints > SK.constraint.n); +} + +bool SolveSpaceUI::GroupsInOrder(hGroup before, hGroup after) { + if(before.v == 0) return true; + if(after.v == 0) return true; + if(!GroupExists(before)) return false; + if(!GroupExists(after)) return false; + int beforep = SK.GetGroup(before)->order; + int afterp = SK.GetGroup(after)->order; + if(beforep >= afterp) return false; + return true; +} + +bool SolveSpaceUI::GroupExists(hGroup hg) { + // A nonexistent group is not acceptable + return SK.group.FindByIdNoOops(hg) ? true : false; +} +bool SolveSpaceUI::EntityExists(hEntity he) { + // A nonexstient entity is acceptable, though, usually just means it + // doesn't apply. + if(he == Entity::NO_ENTITY) return true; + return SK.entity.FindByIdNoOops(he) ? true : false; +} + +bool SolveSpaceUI::PruneGroups(hGroup hg) { + Group *g = SK.GetGroup(hg); + if(GroupsInOrder(g->opA, hg) && + EntityExists(g->predef.origin) && + EntityExists(g->predef.entityB) && + EntityExists(g->predef.entityC)) + { + return false; + } + (deleted.groups)++; + SK.group.RemoveById(g->h); + return true; +} + +bool SolveSpaceUI::PruneRequestsAndConstraints(hGroup hg) { + auto entityRequestExists = [](hEntity he, bool checkEntity = false) { + if(he == Entity::NO_ENTITY) { + return true; + } + + if(he.isFromRequest()) { + if(SK.request.FindByIdNoOops(he.request()) != nullptr) { + return true; + } + } else if(!checkEntity || SK.entity.FindByIdNoOops(he) != nullptr) { + return true; + } + + return false; + }; + + const int requests = SK.request.n; + for(Request &r : SK.request) { + if(r.group != hg) { + continue; + } + + if(entityRequestExists(r.workplane)) { + continue; + } + + r.tag = 1; + } + SK.request.RemoveTagged(); + deleted.requests += requests - SK.request.n; + + const int constraints = SK.constraint.n; + for(Constraint &c : SK.constraint) { + if(c.group != hg) + continue; + + if(entityRequestExists(c.workplane, true) && + entityRequestExists(c.ptA, true) && + entityRequestExists(c.ptB, true) && + entityRequestExists(c.entityA, true) && + entityRequestExists(c.entityB, true) && + entityRequestExists(c.entityC, true) && + entityRequestExists(c.entityD, true)) { + continue; + } + + if(c.type != Constraint::Type::POINTS_COINCIDENT && + c.type != Constraint::Type::HORIZONTAL && + c.type != Constraint::Type::VERTICAL) { + (deleted.nonTrivialConstraints)++; + } + + c.tag = 1; + } + SK.constraint.RemoveTagged(); + deleted.constraints += constraints - SK.constraint.n; + + return (requests > SK.request.n) || (constraints > SK.constraint.n); +} + +void SolveSpaceUI::GenerateAll(Generate type, bool andFindFree, bool genForBBox) { + int first = 0, last = 0, i; + + uint64_t startMillis = GetMilliseconds(), + endMillis; + + SK.groupOrder.Clear(); + for(auto &g : SK.group) { SK.groupOrder.Add(&g.h); } + std::sort(SK.groupOrder.begin(), SK.groupOrder.end(), + [](const hGroup &ha, const hGroup &hb) { + return SK.GetGroup(ha)->order < SK.GetGroup(hb)->order; + }); + + switch(type) { + case Generate::DIRTY: { + first = INT_MAX; + last = 0; + + // Start from the first dirty group, and solve until the active group, + // since all groups after the active group are hidden. + // Not using range-for because we're tracking the indices. + for(i = 0; i < SK.groupOrder.n; i++) { + Group *g = SK.GetGroup(SK.groupOrder[i]); + if((!g->clean) || !g->IsSolvedOkay()) { + first = min(first, i); + } + if(g->h == SS.GW.activeGroup) { + last = i; + } + } + if(first == INT_MAX || last == 0) { + // All clean; so just regenerate the entities, and don't solve anything. + first = -1; + last = -1; + } else { + SS.nakedEdges.Clear(); + } + break; + } + + case Generate::ALL: + first = 0; + last = INT_MAX; + break; + + case Generate::REGEN: + first = -1; + last = -1; + break; + + case Generate::UNTIL_ACTIVE: { + for(i = 0; i < SK.groupOrder.n; i++) { + if(SK.groupOrder[i] == SS.GW.activeGroup) + break; + } + + first = 0; + last = i; + break; + } + } + + // If we're generating entities for display, first we need to find + // the bounding box to turn relative chord tolerance to absolute. + if(!SS.exportMode && !genForBBox) { + GenerateAll(type, andFindFree, /*genForBBox=*/true); + BBox box = SK.CalculateEntityBBox(/*includeInvisibles=*/true); + Vector size = box.maxp.Minus(box.minp); + double maxSize = std::max({ size.x, size.y, size.z }); + chordTolCalculated = maxSize * chordTol / 100.0; + } + + // Remove any requests or constraints that refer to a nonexistent + // group; can check those immediately, since we know what the list + // of groups should be. + PruneOrphans(); + + // Don't lose our numerical guesses when we regenerate. + ParamList prev = {}; + SK.param.MoveSelfInto(&prev); + SK.param.ReserveMore(prev.n); + int oldEntityCount = SK.entity.n; + SK.entity.Clear(); + SK.entity.ReserveMore(oldEntityCount); + + // Not using range-for because we're using the index inside the loop. + for(i = 0; i < SK.groupOrder.n; i++) { + hGroup hg = SK.groupOrder[i]; + + // The group may depend on entities or other groups, to define its + // workplane geometry or for its operands. Those must already exist + // in a previous group, so check them before generating. + if(PruneGroups(hg)) + goto pruned; + + int groupRequestIndex = 0; + for(auto &req : SK.request) { + Request *r = &req; + if(r->group != hg) continue; + r->groupRequestIndex = groupRequestIndex++; + + r->Generate(&(SK.entity), &(SK.param)); + } + for(auto &con : SK.constraint) { + Constraint *c = &con; + if(c->group != hg) continue; + + c->Generate(&(SK.param)); + } + SK.GetGroup(hg)->Generate(&(SK.entity), &(SK.param)); + + // The requests and constraints depend on stuff in this or the + // previous group, so check them after generating. + if(PruneRequestsAndConstraints(hg)) + goto pruned; + + // Use the previous values for params that we've seen before, as + // initial guesses for the solver. + for(auto &p : SK.param) { + Param *newp = &p; + if(newp->known) continue; + + Param *prevp = prev.FindByIdNoOops(newp->h); + if(prevp) { + newp->val = prevp->val; + newp->free = prevp->free; + } + } + + if(hg == Group::HGROUP_REFERENCES) { + ForceReferences(); + Group *g = SK.GetGroup(hg); + g->solved.how = SolveResult::OKAY; + g->clean = true; + } else { + // this i is an index in groupOrder + if(i >= first && i <= last) { + // The group falls inside the range, so really solve it, + // and then regenerate the mesh based on the solved stuff. + Group *g = SK.GetGroup(hg); + if(genForBBox) { + SolveGroupAndReport(hg, andFindFree); + g->GenerateLoops(); + } else { + g->GenerateShellAndMesh(); + g->clean = true; + } + } else { + // The group falls outside the range, so just assume that + // it's good wherever we left it. The mesh is unchanged, + // and the parameters must be marked as known. + for(auto &p : SK.param) { + Param *newp = &p; + + Param *prevp = prev.FindByIdNoOops(newp->h); + if(prevp) newp->known = true; + } + } + } + } + + // And update any reference dimensions with their new values + for(auto &con : SK.constraint) { + Constraint *c = &con; + if(c->reference) { + c->ModifyToSatisfy(); + } + } + + // Make sure the point that we're tracing exists. + if(traced.point.v && !SK.entity.FindByIdNoOops(traced.point)) { + traced.point = Entity::NO_ENTITY; + } + // And if we're tracing a point, add its new value to the path + if(traced.point.v) { + Entity *pt = SK.GetEntity(traced.point); + traced.path.AddPoint(pt->PointGetNum()); + } + + prev.Clear(); + GW.Invalidate(); + + // Remove nonexistent selection items, for same reason we waited till + // the end to put up a dialog box. + GW.ClearNonexistentSelectionItems(); + + if(deleted.requests > 0 || deleted.constraints > 0 || deleted.groups > 0) { + // All sorts of interesting things could have happened; for example, + // the active group or active workplane could have been deleted. So + // clear all that out. + if(deleted.groups > 0) { + SS.TW.ClearSuper(); + } + ScheduleShowTW(); + GW.ClearSuper(); + + auto deletedStat = deleted; + deleted = {}; + + // People get annoyed if I complain whenever they delete any request, + // and I otherwise will, since those always come with pt-coincident + // constraints. + if(deletedStat.requests > 0 || deletedStat.nonTrivialConstraints > 0 || + deletedStat.groups > 0) + { + // Don't display any errors until we've regenerated fully. The + // sketch is not necessarily in a consistent state until we've + // pruned any orphaned etc. objects, and the message loop for the + // messagebox could allow us to repaint and crash. But now we must + // be fine. + Message("Additional sketch elements were deleted, because they " + "depend on the element that was just deleted explicitly. " + "These include: \n" + " %d request%s\n" + " %d constraint%s\n" + " %d group%s" + "%s", + deletedStat.requests, deletedStat.requests == 1 ? "" : "s", + deletedStat.constraints, deletedStat.constraints == 1 ? "" : "s", + deletedStat.groups, deletedStat.groups == 1 ? "" : "s", + undo.cnt > 0 ? "\n\nChoose Edit -> Undo to undelete all elements." : ""); + } + } + + Platform::FreeAllTemporary(); + allConsistent = true; + SS.GW.persistentDirty = true; + SS.centerOfMass.dirty = true; + + endMillis = GetMilliseconds(); + + if(endMillis - startMillis > 30) { + const char *typeStr = ""; + switch(type) { + case Generate::DIRTY: typeStr = "DIRTY"; break; + case Generate::ALL: typeStr = "ALL"; break; + case Generate::REGEN: typeStr = "REGEN"; break; + case Generate::UNTIL_ACTIVE: typeStr = "UNTIL_ACTIVE"; break; + } + if(endMillis) + dbp("Generate::%s%s took %lld ms", + typeStr, + (genForBBox ? " (for bounding box)" : ""), + GetMilliseconds() - startMillis); + } + + return; + +pruned: + // Restore the numerical guesses + SK.param.Clear(); + prev.MoveSelfInto(&(SK.param)); + // Try again + GenerateAll(type, andFindFree, genForBBox); +} + +void SolveSpaceUI::ForceReferences() { + // Force the values of the parameters that define the three reference + // coordinate systems. + static const struct { + hRequest hr; + Quaternion q; + } Quat[] = { + { Request::HREQUEST_REFERENCE_XY, { 1, 0, 0, 0, } }, + { Request::HREQUEST_REFERENCE_YZ, { 0.5, 0.5, 0.5, 0.5, } }, + { Request::HREQUEST_REFERENCE_ZX, { 0.5, -0.5, -0.5, -0.5, } }, + }; + for(int i = 0; i < 3; i++) { + hRequest hr = Quat[i].hr; + Entity *wrkpl = SK.GetEntity(hr.entity(0)); + // The origin for our coordinate system, always zero + Entity *origin = SK.GetEntity(wrkpl->point[0]); + origin->PointForceTo(Vector::From(0, 0, 0)); + origin->construction = true; + SK.GetParam(origin->param[0])->known = true; + SK.GetParam(origin->param[1])->known = true; + SK.GetParam(origin->param[2])->known = true; + // The quaternion that defines the rotation, from the table. + Entity *normal = SK.GetEntity(wrkpl->normal); + normal->NormalForceTo(Quat[i].q); + SK.GetParam(normal->param[0])->known = true; + SK.GetParam(normal->param[1])->known = true; + SK.GetParam(normal->param[2])->known = true; + SK.GetParam(normal->param[3])->known = true; + } +} + +void SolveSpaceUI::UpdateCenterOfMass() { + SMesh *m = &(SK.GetGroup(SS.GW.activeGroup)->displayMesh); + SS.centerOfMass.position = m->GetCenterOfMass(); + SS.centerOfMass.dirty = false; +} + +void SolveSpaceUI::MarkDraggedParams() { + sys.dragged.clear(); + + for(int i = -1; i < SS.GW.pending.points.n; i++) { + hEntity hp; + if(i == -1) { + hp = SS.GW.pending.point; + } else { + hp = SS.GW.pending.points[i]; + } + if(!hp.v) continue; + + // The pending point could be one in a group that has not yet + // been processed, in which case the lookup will fail; but + // that's not an error. + Entity *pt = SK.entity.FindByIdNoOops(hp); + if(pt) { + switch(pt->type) { + case Entity::Type::POINT_N_TRANS: + case Entity::Type::POINT_IN_3D: + case Entity::Type::POINT_N_ROT_AXIS_TRANS: + sys.dragged.insert(pt->param[0]); + sys.dragged.insert(pt->param[1]); + sys.dragged.insert(pt->param[2]); + break; + + case Entity::Type::POINT_IN_2D: + sys.dragged.insert(pt->param[0]); + sys.dragged.insert(pt->param[1]); + break; + + default: // Only the entities above can be dragged. + break; + } + } + } + if(SS.GW.pending.circle.v) { + Entity *circ = SK.entity.FindByIdNoOops(SS.GW.pending.circle); + if(circ) { + Entity *dist = SK.GetEntity(circ->distance); + switch(dist->type) { + case Entity::Type::DISTANCE: + sys.dragged.insert(dist->param[0]); + break; + + default: // Only the entities above can be dragged. + break; + } + } + } + if(SS.GW.pending.normal.v) { + Entity *norm = SK.entity.FindByIdNoOops(SS.GW.pending.normal); + if(norm) { + switch(norm->type) { + case Entity::Type::NORMAL_IN_3D: + sys.dragged.insert(norm->param[0]); + sys.dragged.insert(norm->param[1]); + sys.dragged.insert(norm->param[2]); + sys.dragged.insert(norm->param[3]); + break; + + default: // Only the entities above can be dragged. + break; + } + } + } +} + +void SolveSpaceUI::SolveGroupAndReport(hGroup hg, bool andFindFree) { + SolveGroup(hg, andFindFree); + + Group *g = SK.GetGroup(hg); + bool isOkay = g->solved.how == SolveResult::OKAY || + (g->allowRedundant && g->solved.how == SolveResult::REDUNDANT_OKAY); + if(!isOkay || (isOkay && !g->IsSolvedOkay())) { + TextWindow::ReportHowGroupSolved(g->h); + } +} + +void SolveSpaceUI::WriteEqSystemForGroup(hGroup hg) { + // Clear out the system to be solved. + sys.entity.Clear(); + sys.param.Clear(); + sys.eq.Clear(); + // And generate all the params for requests in this group + for(auto &req : SK.request) { + Request *r = &req; + if(r->group != hg) continue; + + r->Generate(&(sys.entity), &(sys.param)); + } + for(auto &con : SK.constraint) { + Constraint *c = &con; + if(c->group != hg) continue; + + c->Generate(&(sys.param)); + } + // And for the group itself + Group *g = SK.GetGroup(hg); + g->Generate(&(sys.entity), &(sys.param)); + // Set the initial guesses for all the params + for(auto ¶m : sys.param) { + Param *p = ¶m; + p->known = false; + p->val = SK.GetParam(p->h)->val; + } + + MarkDraggedParams(); +} + +void SolveSpaceUI::SolveGroup(hGroup hg, bool andFindFree) { + WriteEqSystemForGroup(hg); + Group *g = SK.GetGroup(hg); + g->solved.remove.Clear(); + g->solved.findToFixTimeout = SS.timeoutRedundantConstr; + SolveResult how = sys.Solve(g, &(g->solved.dof), + &(g->solved.remove), + /*andFindBad=*/!g->allowRedundant, + /*andFindFree=*/andFindFree, + /*forceDofCheck=*/!g->dofCheckOk); + if(how == SolveResult::OKAY) { + g->dofCheckOk = true; + } + g->solved.how = how; + Platform::FreeAllTemporary(); +} + +SolveResult SolveSpaceUI::TestRankForGroup(hGroup hg, int *rank) { + Group *g = SK.GetGroup(hg); + // If we don't calculate dof or redundant is allowed, there is + // no point to solve rank because this result is not meaningful + if(g->suppressDofCalculation || g->allowRedundant) return SolveResult::OKAY; + WriteEqSystemForGroup(hg); + SolveResult result = sys.SolveRank(g, rank); + Platform::FreeAllTemporary(); + return result; +} + +bool SolveSpaceUI::ActiveGroupsOkay() { + for(int i = 0; i < SK.groupOrder.n; i++) { + Group *g = SK.GetGroup(SK.groupOrder[i]); + if(!g->IsSolvedOkay()) + return false; + if(g->h == SS.GW.activeGroup) + break; + } + return true; +} + +} // namespace SolveSpace diff --git a/src/graphicswin.cpp b/src/graphicswin.cpp new file mode 100644 index 0000000..98ceb73 --- /dev/null +++ b/src/graphicswin.cpp @@ -0,0 +1,1481 @@ +//----------------------------------------------------------------------------- +// Top-level implementation of the program's main window, in which a graphical +// representation of the model is drawn and edited by the user. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +typedef void MenuHandler(Command id); +using MenuKind = Platform::MenuItem::Indicator; +struct MenuEntry { + int level; // 0 == on menu bar, 1 == one level down + const char *label; // or NULL for a separator + Command cmd; // command ID + int accel; // keyboard accelerator + MenuKind kind; + MenuHandler *fn; +}; + +#define mView (&GraphicsWindow::MenuView) +#define mEdit (&GraphicsWindow::MenuEdit) +#define mClip (&GraphicsWindow::MenuClipboard) +#define mReq (&GraphicsWindow::MenuRequest) +#define mCon (&Constraint::MenuConstrain) +#define mFile (&SolveSpaceUI::MenuFile) +#define mGrp (&Group::MenuGroup) +#define mAna (&SolveSpaceUI::MenuAnalyze) +#define mHelp (&SolveSpaceUI::MenuHelp) +#define SHIFT_MASK 0x100 +#define CTRL_MASK 0x200 +#define FN_MASK 0x400 + +#define S SHIFT_MASK +#define C CTRL_MASK +#define F FN_MASK +#define KN MenuKind::NONE +#define KC MenuKind::CHECK_MARK +#define KR MenuKind::RADIO_MARK +const MenuEntry Menu[] = { +//lv label cmd accel kind +{ 0, N_("&File"), Command::NONE, 0, KN, NULL }, +{ 1, N_("&New"), Command::NEW, C|'n', KN, mFile }, +{ 1, N_("&Open..."), Command::OPEN, C|'o', KN, mFile }, +{ 1, N_("Open &Recent"), Command::OPEN_RECENT, 0, KN, mFile }, +{ 1, N_("&Save"), Command::SAVE, C|'s', KN, mFile }, +{ 1, N_("Save &As..."), Command::SAVE_AS, C|S|'s', KN, mFile }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("Export &Image..."), Command::EXPORT_IMAGE, 0, KN, mFile }, +{ 1, N_("Export 2d &View..."), Command::EXPORT_VIEW, 0, KN, mFile }, +{ 1, N_("Export 2d &Section..."), Command::EXPORT_SECTION, 0, KN, mFile }, +{ 1, N_("Export 3d &Wireframe..."), Command::EXPORT_WIREFRAME, 0, KN, mFile }, +{ 1, N_("Export Triangle &Mesh..."), Command::EXPORT_MESH, 0, KN, mFile }, +{ 1, N_("Export &Surfaces..."), Command::EXPORT_SURFACES, 0, KN, mFile }, +{ 1, N_("Im&port..."), Command::IMPORT, 0, KN, mFile }, +#ifndef __APPLE__ +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("E&xit"), Command::EXIT, C|'q', KN, mFile }, +#endif + +{ 0, N_("&Edit"), Command::NONE, 0, KN, NULL }, +{ 1, N_("&Undo"), Command::UNDO, C|'z', KN, mEdit }, +{ 1, N_("&Redo"), Command::REDO, C|'y', KN, mEdit }, +{ 1, N_("Re&generate All"), Command::REGEN_ALL, ' ', KN, mEdit }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("Snap Selection to &Grid"), Command::SNAP_TO_GRID, '.', KN, mEdit }, +{ 1, N_("Rotate Imported &90°"), Command::ROTATE_90, '9', KN, mEdit }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("Cu&t"), Command::CUT, C|'x', KN, mClip }, +{ 1, N_("&Copy"), Command::COPY, C|'c', KN, mClip }, +{ 1, N_("&Paste"), Command::PASTE, C|'v', KN, mClip }, +{ 1, N_("Paste &Transformed..."), Command::PASTE_TRANSFORM, C|'t', KN, mClip }, +{ 1, N_("&Delete"), Command::DELETE, '\x7f', KN, mClip }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("Select &Edge Chain"), Command::SELECT_CHAIN, C|'e', KN, mEdit }, +{ 1, N_("Select &All"), Command::SELECT_ALL, C|'a', KN, mEdit }, +{ 1, N_("&Unselect All"), Command::UNSELECT_ALL, '\x1b', KN, mEdit }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("&Line Styles..."), Command::EDIT_LINE_STYLES, 0, KN, mEdit }, +{ 1, N_("&View Projection..."), Command::VIEW_PROJECTION, 0, KN, mEdit }, +#ifndef __APPLE__ +{ 1, N_("Con&figuration..."), Command::CONFIGURATION, 0, KN, mEdit }, +#endif + +{ 0, N_("&View"), Command::NONE, 0, KN, mView }, +{ 1, N_("Zoom &In"), Command::ZOOM_IN, '+', KN, mView }, +{ 1, N_("Zoom &Out"), Command::ZOOM_OUT, '-', KN, mView }, +{ 1, N_("Zoom To &Fit"), Command::ZOOM_TO_FIT, 'f', KN, mView }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("Align View to &Workplane"), Command::ONTO_WORKPLANE, 'w', KN, mView }, +{ 1, N_("Nearest &Ortho View"), Command::NEAREST_ORTHO, F|2, KN, mView }, +{ 1, N_("Nearest &Isometric View"), Command::NEAREST_ISO, F|3, KN, mView }, +{ 1, N_("&Center View At Point"), Command::CENTER_VIEW, F|4, KN, mView }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("Show Snap &Grid"), Command::SHOW_GRID, '>', KC, mView }, +{ 1, N_("Darken Inactive Solids"), Command::DIM_SOLID_MODEL, 0, KC, mView }, +{ 1, N_("Use &Perspective Projection"), Command::PERSPECTIVE_PROJ, '`', KC, mView }, +{ 1, N_("Show E&xploded View"), Command::EXPLODE_SKETCH, '\\', KC, mView }, +{ 1, N_("Dimension &Units"), Command::NONE, 0, KN, NULL }, +{ 2, N_("Dimensions in &Millimeters"), Command::UNITS_MM, 0, KR, mView }, +{ 2, N_("Dimensions in M&eters"), Command::UNITS_METERS, 0, KR, mView }, +{ 2, N_("Dimensions in &Inches"), Command::UNITS_INCHES, 0, KR, mView }, +{ 2, N_("Dimensions in &Feet and Inches"), Command::UNITS_FEET_INCHES, 0, KR, mView }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("Show &Toolbar"), Command::SHOW_TOOLBAR, C|'\t', KC, mView }, +{ 1, N_("Show Property Bro&wser"), Command::SHOW_TEXT_WND, '\t', KC, mView }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("&Full Screen"), Command::FULL_SCREEN, C|F|11, KC, mView }, + +{ 0, N_("&New Group"), Command::NONE, 0, KN, mGrp }, +{ 1, N_("Sketch In &3d"), Command::GROUP_3D, S|'3', KN, mGrp }, +{ 1, N_("Sketch In New &Workplane"), Command::GROUP_WRKPL, S|'w', KN, mGrp }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("Step &Translating"), Command::GROUP_TRANS, S|'t', KN, mGrp }, +{ 1, N_("Step &Rotating"), Command::GROUP_ROT, S|'r', KN, mGrp }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("E&xtrude"), Command::GROUP_EXTRUDE, S|'x', KN, mGrp }, +{ 1, N_("&Helix"), Command::GROUP_HELIX, S|'h', KN, mGrp }, +{ 1, N_("&Lathe"), Command::GROUP_LATHE, S|'l', KN, mGrp }, +{ 1, N_("Re&volve"), Command::GROUP_REVOLVE, S|'v', KN, mGrp }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("Link / Assemble..."), Command::GROUP_LINK, S|'i', KN, mGrp }, +{ 1, N_("Link Recent"), Command::GROUP_RECENT, 0, KN, mGrp }, + +{ 0, N_("&Sketch"), Command::NONE, 0, KN, mReq }, +{ 1, N_("In &Workplane"), Command::SEL_WORKPLANE, '2', KR, mReq }, +{ 1, N_("Anywhere In &3d"), Command::FREE_IN_3D, '3', KR, mReq }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("Datum &Point"), Command::DATUM_POINT, 'p', KN, mReq }, +{ 1, N_("Wor&kplane"), Command::WORKPLANE, 0, KN, mReq }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("Line &Segment"), Command::LINE_SEGMENT, 's', KN, mReq }, +{ 1, N_("C&onstruction Line Segment"), Command::CONSTR_SEGMENT, S|'s', KN, mReq }, +{ 1, N_("&Rectangle"), Command::RECTANGLE, 'r', KN, mReq }, +{ 1, N_("&Circle"), Command::CIRCLE, 'c', KN, mReq }, +{ 1, N_("&Arc of a Circle"), Command::ARC, 'a', KN, mReq }, +{ 1, N_("&Bezier Cubic Spline"), Command::CUBIC, 'b', KN, mReq }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("&Text in TrueType Font"), Command::TTF_TEXT, 't', KN, mReq }, +{ 1, N_("I&mage"), Command::IMAGE, 0, KN, mReq }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("To&ggle Construction"), Command::CONSTRUCTION, 'g', KN, mReq }, +{ 1, N_("Ta&ngent Arc at Point"), Command::TANGENT_ARC, S|'a', KN, mReq }, +{ 1, N_("Split Curves at &Intersection"), Command::SPLIT_CURVES, 'i', KN, mReq }, + +{ 0, N_("&Constrain"), Command::NONE, 0, KN, mCon }, +{ 1, N_("&Distance / Diameter"), Command::DISTANCE_DIA, 'd', KN, mCon }, +{ 1, N_("Re&ference Dimension"), Command::REF_DISTANCE, S|'d', KN, mCon }, +{ 1, N_("A&ngle / Equal Angle"), Command::ANGLE, 'n', KN, mCon }, +{ 1, N_("Reference An&gle"), Command::REF_ANGLE, S|'n', KN, mCon }, +{ 1, N_("Other S&upplementary Angle"), Command::OTHER_ANGLE, 'u', KN, mCon }, +{ 1, N_("Toggle R&eference Dim"), Command::REFERENCE, 'e', KN, mCon }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("&Horizontal"), Command::HORIZONTAL, 'h', KN, mCon }, +{ 1, N_("&Vertical"), Command::VERTICAL, 'v', KN, mCon }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("&On Point / Curve / Plane"), Command::ON_ENTITY, 'o', KN, mCon }, +{ 1, N_("E&qual Length / Radius"), Command::EQUAL, 'q', KN, mCon }, +{ 1, N_("Length / Arc Ra&tio"), Command::RATIO, 'z', KN, mCon }, +{ 1, N_("Length / Arc Diff&erence"), Command::DIFFERENCE, 'j', KN, mCon }, +{ 1, N_("At &Midpoint"), Command::AT_MIDPOINT, 'm', KN, mCon }, +{ 1, N_("S&ymmetric"), Command::SYMMETRIC, 'y', KN, mCon }, +{ 1, N_("Para&llel / Tangent"), Command::PARALLEL, 'l', KN, mCon }, +{ 1, N_("&Perpendicular"), Command::PERPENDICULAR, '[', KN, mCon }, +{ 1, N_("Same Orient&ation"), Command::ORIENTED_SAME, 'x', KN, mCon }, +{ 1, N_("Lock Point Where &Dragged"), Command::WHERE_DRAGGED, ']', KN, mCon }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("Comment"), Command::COMMENT, ';', KN, mCon }, + +{ 0, N_("&Analyze"), Command::NONE, 0, KN, mAna }, +{ 1, N_("Measure &Volume"), Command::VOLUME, C|S|'v', KN, mAna }, +{ 1, N_("Measure A&rea"), Command::AREA, C|S|'a', KN, mAna }, +{ 1, N_("Measure &Perimeter"), Command::PERIMETER, C|S|'p', KN, mAna }, +{ 1, N_("Show &Interfering Parts"), Command::INTERFERENCE, C|S|'i', KN, mAna }, +{ 1, N_("Show &Naked Edges"), Command::NAKED_EDGES, C|S|'n', KN, mAna }, +{ 1, N_("Show &Center of Mass"), Command::CENTER_OF_MASS, C|S|'c', KN, mAna }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("Show &Underconstrained Points"), Command::SHOW_DOF, C|S|'f', KN, mAna }, +{ 1, NULL, Command::NONE, 0, KN, NULL }, +{ 1, N_("&Trace Point"), Command::TRACE_PT, C|S|'t', KN, mAna }, +{ 1, N_("&Stop Tracing..."), Command::STOP_TRACING, C|S|'s', KN, mAna }, +{ 1, N_("Step &Dimension..."), Command::STEP_DIM, C|S|'d', KN, mAna }, + +{ 0, N_("&Help"), Command::NONE, 0, KN, mHelp }, +{ 1, N_("&Language"), Command::LOCALE, 0, KN, mHelp }, +{ 1, N_("&Website / Manual"), Command::WEBSITE, 0, KN, mHelp }, +{ 1, N_("&Go to GitHub commit"), Command::GITHUB, 0, KN, mHelp }, +#ifndef __APPLE__ +{ 1, N_("&About"), Command::ABOUT, 0, KN, mHelp }, +#endif +{ -1, 0, Command::NONE, 0, KN, NULL } +}; +#undef S +#undef C +#undef F +#undef KN +#undef KC +#undef KR + +void GraphicsWindow::ActivateCommand(Command cmd) { + for(int i = 0; Menu[i].level >= 0; i++) { + if(cmd == Menu[i].cmd) { + (Menu[i].fn)((Command)Menu[i].cmd); + break; + } + } +} + +Platform::KeyboardEvent GraphicsWindow::AcceleratorForCommand(Command cmd) { + int rawAccel = 0; + for(int i = 0; Menu[i].level >= 0; i++) { + if(cmd == Menu[i].cmd) { + rawAccel = Menu[i].accel; + break; + } + } + + Platform::KeyboardEvent accel = {}; + accel.type = Platform::KeyboardEvent::Type::PRESS; + if(rawAccel & SHIFT_MASK) { + accel.shiftDown = true; + } + if(rawAccel & CTRL_MASK) { + accel.controlDown = true; + } + if(rawAccel & FN_MASK) { + accel.key = Platform::KeyboardEvent::Key::FUNCTION; + accel.num = rawAccel & 0xff; + } else { + accel.key = Platform::KeyboardEvent::Key::CHARACTER; + accel.chr = (char)(rawAccel & 0xff); + } + + return accel; +} + +bool GraphicsWindow::KeyboardEvent(Platform::KeyboardEvent event) { + using Platform::KeyboardEvent; + + if(event.type == KeyboardEvent::Type::RELEASE) + return true; + + if(event.key == KeyboardEvent::Key::CHARACTER) { + if(event.chr == '\b') { + // Treat backspace identically to escape. + MenuEdit(Command::UNSELECT_ALL); + return true; + } else if(event.chr == '=') { + // Treat = as +. This is specific to US (and US-compatible) keyboard layouts, + // but makes zooming from keyboard much more usable on these. + // Ideally we'd have a platform-independent way of binding to a particular + // physical key regardless of shift status... + MenuView(Command::ZOOM_IN); + return true; + } + } + + // On some platforms, the OS does not handle some or all keyboard accelerators, + // so handle them here. + for(int i = 0; Menu[i].level >= 0; i++) { + if(AcceleratorForCommand(Menu[i].cmd).Equals(event)) { + ActivateCommand(Menu[i].cmd); + return true; + } + } + + return false; +} + +void GraphicsWindow::PopulateMainMenu() { + bool unique = false; + Platform::MenuBarRef mainMenu = Platform::GetOrCreateMainMenu(&unique); + if(unique) mainMenu->Clear(); + + Platform::MenuRef currentSubMenu; + std::vector subMenuStack; + for(int i = 0; Menu[i].level >= 0; i++) { + while(Menu[i].level > 0 && Menu[i].level <= (int)subMenuStack.size()) { + currentSubMenu = subMenuStack.back(); + subMenuStack.pop_back(); + } + + if(Menu[i].label == NULL) { + currentSubMenu->AddSeparator(); + continue; + } + + std::string label = Translate(Menu[i].label); + if(Menu[i].level == 0) { + currentSubMenu = mainMenu->AddSubMenu(label); + } else if(Menu[i].cmd == Command::OPEN_RECENT) { + openRecentMenu = currentSubMenu->AddSubMenu(label); + } else if(Menu[i].cmd == Command::GROUP_RECENT) { + linkRecentMenu = currentSubMenu->AddSubMenu(label); + } else if(Menu[i].cmd == Command::LOCALE) { + Platform::MenuRef localeMenu = currentSubMenu->AddSubMenu(label); + for(const Locale &locale : Locales()) { + localeMenu->AddItem(locale.displayName, [&]() { + SetLocale(locale.Culture()); + Platform::GetSettings()->FreezeString("Locale", locale.Culture()); + + SS.UpdateWindowTitles(); + PopulateMainMenu(); + SS.GW.EnsureValidActives(); + }); + } + } else if(Menu[i].fn == NULL) { + subMenuStack.push_back(currentSubMenu); + currentSubMenu = currentSubMenu->AddSubMenu(label); + } else { + Platform::MenuItemRef menuItem = currentSubMenu->AddItem(label); + menuItem->SetIndicator(Menu[i].kind); + if(Menu[i].accel != 0) { + menuItem->SetAccelerator(AcceleratorForCommand(Menu[i].cmd)); + } + menuItem->onTrigger = std::bind(Menu[i].fn, Menu[i].cmd); + + if(Menu[i].cmd == Command::SHOW_GRID) { + showGridMenuItem = menuItem; + } else if(Menu[i].cmd == Command::DIM_SOLID_MODEL) { + dimSolidModelMenuItem = menuItem; + } else if(Menu[i].cmd == Command::PERSPECTIVE_PROJ) { + perspectiveProjMenuItem = menuItem; + } else if(Menu[i].cmd == Command::EXPLODE_SKETCH) { + explodeMenuItem = menuItem; + } else if(Menu[i].cmd == Command::SHOW_TOOLBAR) { + showToolbarMenuItem = menuItem; + } else if(Menu[i].cmd == Command::SHOW_TEXT_WND) { + showTextWndMenuItem = menuItem; + } else if(Menu[i].cmd == Command::FULL_SCREEN) { + fullScreenMenuItem = menuItem; + } else if(Menu[i].cmd == Command::UNITS_MM) { + unitsMmMenuItem = menuItem; + } else if(Menu[i].cmd == Command::UNITS_METERS) { + unitsMetersMenuItem = menuItem; + } else if(Menu[i].cmd == Command::UNITS_INCHES) { + unitsInchesMenuItem = menuItem; + } else if(Menu[i].cmd == Command::UNITS_FEET_INCHES) { + unitsFeetInchesMenuItem = menuItem; + } else if(Menu[i].cmd == Command::SEL_WORKPLANE) { + inWorkplaneMenuItem = menuItem; + } else if(Menu[i].cmd == Command::FREE_IN_3D) { + in3dMenuItem = menuItem; + } else if(Menu[i].cmd == Command::UNDO) { + undoMenuItem = menuItem; + } else if(Menu[i].cmd == Command::REDO) { + redoMenuItem = menuItem; + } + } + } + + PopulateRecentFiles(); + SS.UndoEnableMenus(); + + window->SetMenuBar(mainMenu); +} + +static void PopulateMenuWithPathnames(Platform::MenuRef menu, + std::vector pathnames, + std::function onTrigger) { + menu->Clear(); + if(pathnames.empty()) { + Platform::MenuItemRef menuItem = menu->AddItem(_("(no recent files)")); + menuItem->SetEnabled(false); + } else { + for(Platform::Path pathname : pathnames) { + Platform::MenuItemRef menuItem = menu->AddItem(pathname.raw, [=]() { + if(FileExists(pathname)) { + onTrigger(pathname); + } else { + Error(_("File '%s' does not exist."), pathname.raw.c_str()); + } + }, /*mnemonics=*/false); + } + } +} + +void GraphicsWindow::PopulateRecentFiles() { + PopulateMenuWithPathnames(openRecentMenu, SS.recentFiles, [](const Platform::Path &path) { + // OkayToStartNewFile could mutate recentFiles, which will invalidate path (which is a + // reference into the recentFiles vector), so take a copy of it here. + Platform::Path pathCopy(path); + if(!SS.OkayToStartNewFile()) return; + SS.Load(pathCopy); + }); + + PopulateMenuWithPathnames(linkRecentMenu, SS.recentFiles, [](const Platform::Path &path) { + Group::MenuGroup(Command::GROUP_LINK, path); + }); +} + +void GraphicsWindow::Init() { + scale = 5; + offset = Vector::From(0, 0, 0); + projRight = Vector::From(1, 0, 0); + projUp = Vector::From(0, 1, 0); + + // Make sure those are valid; could get a mouse move without a mouse + // down if someone depresses the button, then drags into our window. + orig.projRight = projRight; + orig.projUp = projUp; + + // And with the last group active + ssassert(!SK.groupOrder.IsEmpty(), + "Group order can't be empty since we will activate the last group."); + activeGroup = *SK.groupOrder.Last(); + SK.GetGroup(activeGroup)->Activate(); + + showWorkplanes = false; + showNormals = true; + showPoints = true; + showConstruction = true; + showConstraints = ShowConstraintMode::SCM_SHOW_ALL; + showShaded = true; + showEdges = true; + showMesh = false; + showOutlines = false; + showFacesDrawing = false; + showFacesNonDrawing = true; + drawOccludedAs = DrawOccludedAs::INVISIBLE; + + showTextWindow = true; + + showSnapGrid = false; + dimSolidModel = true; + context.active = false; + toolbarHovered = Command::NONE; + + if(!window) { + window = Platform::CreateWindow(); + if(window) { + using namespace std::placeholders; + // Do this first, so that if it causes an onRender event we don't try to paint without + // a canvas. + window->SetMinContentSize(720, /*ToolbarDrawOrHitTest 636*/ 32 * 18 + 3 * 16 + 8 + 4); + window->onClose = std::bind(&SolveSpaceUI::MenuFile, Command::EXIT); + window->onContextLost = [&] { + canvas = NULL; + persistentCanvas = NULL; + persistentDirty = true; + }; + window->onRender = std::bind(&GraphicsWindow::Paint, this); + window->onKeyboardEvent = std::bind(&GraphicsWindow::KeyboardEvent, this, _1); + window->onMouseEvent = std::bind(&GraphicsWindow::MouseEvent, this, _1); + window->onSixDofEvent = std::bind(&GraphicsWindow::SixDofEvent, this, _1); + window->onEditingDone = std::bind(&GraphicsWindow::EditControlDone, this, _1); + PopulateMainMenu(); + } + } + + if(window) { + canvas = CreateRenderer(); + if(canvas) { + persistentCanvas = canvas->CreateBatch(); + persistentDirty = true; + } + } + + // Do this last, so that all the menus get updated correctly. + ClearSuper(); +} + +void GraphicsWindow::AnimateOntoWorkplane() { + if(!LockedInWorkplane()) return; + + Entity *w = SK.GetEntity(ActiveWorkplane()); + Quaternion quatf = w->Normal()->NormalGetNum(); + + // Get Z pointing vertical, if we're on turntable nav mode: + if(SS.turntableNav) { + Vector normalRight = quatf.RotationU(); + Vector normalUp = quatf.RotationV(); + Vector normal = normalRight.Cross(normalUp); + if(normalRight.z != 0) { + double theta = atan2(normalUp.z, normalRight.z); + theta -= atan2(1, 0); + normalRight = normalRight.RotatedAbout(normal, theta); + normalUp = normalUp.RotatedAbout(normal, theta); + quatf = Quaternion::From(normalRight, normalUp); + } + } + + Vector offsetf = (SK.GetEntity(w->point[0])->PointGetNum()).ScaledBy(-1); + + // If the view screen is open, then we need to refresh it. + SS.ScheduleShowTW(); + + AnimateOnto(quatf, offsetf); +} + +void GraphicsWindow::AnimateOnto(Quaternion quatf, Vector offsetf) { + // Get our initial orientation and translation. + Quaternion quat0 = Quaternion::From(projRight, projUp); + Vector offset0 = offset; + + // Make sure we take the shorter of the two possible paths. + double mp = (quatf.Minus(quat0)).Magnitude(); + double mm = (quatf.Plus(quat0)).Magnitude(); + if(mp > mm) { + quatf = quatf.ScaledBy(-1); + mp = mm; + } + double mo = (offset0.Minus(offsetf)).Magnitude()*scale; + + // Animate transition, unless it's a tiny move. + int64_t t0 = GetMilliseconds(); + int32_t dt = (mp < 0.01 && mo < 10) ? 0 : + (int32_t)(SS.animationSpeed*0.75*mp + SS.animationSpeed*0.0005*mo); + // Apply a minimum animation time, for small moves. This gets overridden by the maximum setting + // so setting the animation speed to 0 disables animations entirely. + dt = std::max(dt, 100 /* ms */); + // Don't ever animate for longer than animationSpeed ms; we can get absurdly + // long translations (as measured in pixels) if the user zooms out, moves, + // and then zooms in again. + dt = std::min(dt, SS.animationSpeed); + // If the resulting animation time is very short, disable it completely. + if (dt < 10) dt = -20; + + Quaternion dq = quatf.Times(quat0.Inverse()); + + if(!animateTimer) { + animateTimer = Platform::CreateTimer(); + } + animateTimer->onTimeout = [=] { + int64_t tn = GetMilliseconds(); + if((tn - t0) < dt) { + animateTimer->RunAfterNextFrame(); + + double s = (tn - t0)/((double)dt); + offset = (offset0.ScaledBy(1 - s)).Plus(offsetf.ScaledBy(s)); + Quaternion quat = (dq.ToThe(s)).Times(quat0).WithMagnitude(1); + + projRight = quat.RotationU(); + projUp = quat.RotationV(); + } else { + projRight = quatf.RotationU(); + projUp = quatf.RotationV(); + offset = offsetf; + } + window->Invalidate(); + }; + animateTimer->RunAfterNextFrame(); +} + +void GraphicsWindow::HandlePointForZoomToFit(Vector p, Point2d *pmax, Point2d *pmin, + double *wmin, bool usePerspective, + const Camera &camera) +{ + double w; + Vector pp = camera.ProjectPoint4(p, &w); + // If usePerspective is true, then we calculate a perspective projection of the point. + // If not, then we do a parallel projection regardless of the current + // scale factor. + if(usePerspective) { + pp = pp.ScaledBy(1.0/w); + } + + pmax->x = max(pmax->x, pp.x); + pmax->y = max(pmax->y, pp.y); + pmin->x = min(pmin->x, pp.x); + pmin->y = min(pmin->y, pp.y); + *wmin = min(*wmin, w); +} +void GraphicsWindow::LoopOverPoints(const std::vector &entities, + const std::vector &constraints, + const std::vector &faces, + Point2d *pmax, Point2d *pmin, double *wmin, + bool usePerspective, bool includeMesh, + const Camera &camera) { + + for(Entity *e : entities) { + if(e->IsPoint()) { + HandlePointForZoomToFit(e->PointGetNum(), pmax, pmin, wmin, usePerspective, camera); + } else if(e->type == Entity::Type::CIRCLE) { + // Lots of entities can extend outside the bbox of their points, + // but circles are particularly bad. We want to get things halfway + // reasonable without the mesh, because a zoom to fit is used to + // set the zoom level to set the chord tol. + double r = e->CircleGetRadiusNum(); + Vector c = SK.GetEntity(e->point[0])->PointGetNum(); + Quaternion q = SK.GetEntity(e->normal)->NormalGetNum(); + for(int j = 0; j < 4; j++) { + Vector p = (j == 0) ? (c.Plus(q.RotationU().ScaledBy( r))) : + (j == 1) ? (c.Plus(q.RotationU().ScaledBy(-r))) : + (j == 2) ? (c.Plus(q.RotationV().ScaledBy( r))) : + (c.Plus(q.RotationV().ScaledBy(-r))); + HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective, camera); + } + } else { + // We have to iterate children points, because we can select entities without points + for(int i = 0; i < MAX_POINTS_IN_ENTITY; i++) { + if(e->point[i].v == 0) break; + Vector p = SK.GetEntity(e->point[i])->PointGetNum(); + HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective, camera); + } + } + } + + for(Constraint *c : constraints) { + std::vector refs; + c->GetReferencePoints(camera, &refs); + for(const Vector &p : refs) { + HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective, camera); + } + } + + if(!includeMesh && faces.empty()) return; + + Group *g = SK.GetGroup(activeGroup); + g->GenerateDisplayItems(); + for(int i = 0; i < g->displayMesh.l.n; i++) { + STriangle *tr = &(g->displayMesh.l[i]); + if(!includeMesh) { + bool found = false; + for(const hEntity &face : faces) { + if(face.v != tr->meta.face) continue; + found = true; + break; + } + if(!found) continue; + } + HandlePointForZoomToFit(tr->a, pmax, pmin, wmin, usePerspective, camera); + HandlePointForZoomToFit(tr->b, pmax, pmin, wmin, usePerspective, camera); + HandlePointForZoomToFit(tr->c, pmax, pmin, wmin, usePerspective, camera); + } + if(!includeMesh) return; + for(int i = 0; i < g->polyLoops.l.n; i++) { + SContour *sc = &(g->polyLoops.l[i]); + for(int j = 0; j < sc->l.n; j++) { + HandlePointForZoomToFit(sc->l[j].p, pmax, pmin, wmin, usePerspective, camera); + } + } +} +void GraphicsWindow::ZoomToFit(bool includingInvisibles, bool useSelection) { + if(!window) return; + + scale = ZoomToFit(GetCamera(), includingInvisibles, useSelection); +} +double GraphicsWindow::ZoomToFit(const Camera &camera, + bool includingInvisibles, bool useSelection) { + std::vector entities; + std::vector constraints; + std::vector faces; + + if(useSelection) { + for(int i = 0; i < selection.n; i++) { + Selection *s = &selection[i]; + if(s->entity.v != 0) { + Entity *e = SK.entity.FindById(s->entity); + if(e->IsFace()) { + faces.push_back(e->h); + continue; + } + entities.push_back(e); + } + if(s->constraint.v != 0) { + Constraint *c = SK.constraint.FindById(s->constraint); + constraints.push_back(c); + } + } + } + + bool selectionUsed = !entities.empty() || !constraints.empty() || !faces.empty(); + + if(!selectionUsed) { + for(Entity &e : SK.entity) { + // we don't want to handle separate points, because we will iterate them inside entities. + if(e.IsPoint()) continue; + if(!includingInvisibles && !e.IsVisible()) continue; + entities.push_back(&e); + } + + for(Constraint &c : SK.constraint) { + if(!c.IsVisible()) continue; + constraints.push_back(&c); + } + } + + // On the first run, ignore perspective. + Point2d pmax = { -1e12, -1e12 }, pmin = { 1e12, 1e12 }; + double wmin = 1; + LoopOverPoints(entities, constraints, faces, &pmax, &pmin, &wmin, + /*usePerspective=*/false, /*includeMesh=*/!selectionUsed, + camera); + + double xm = (pmax.x + pmin.x)/2, ym = (pmax.y + pmin.y)/2; + double dx = pmax.x - pmin.x, dy = pmax.y - pmin.y; + + offset = offset.Plus(projRight.ScaledBy(-xm)).Plus( + projUp. ScaledBy(-ym)); + + // And based on this, we calculate the scale and offset + double scale; + if(EXACT(dx == 0 && dy == 0)) { + scale = 5; + } else { + double scalex = 1e12, scaley = 1e12; + if(EXACT(dx != 0)) scalex = 0.9*camera.width /dx; + if(EXACT(dy != 0)) scaley = 0.9*camera.height/dy; + scale = min(scalex, scaley); + + scale = min(300.0, scale); + scale = max(0.003, scale); + } + + // Then do another run, considering the perspective. + pmax.x = -1e12; pmax.y = -1e12; + pmin.x = 1e12; pmin.y = 1e12; + wmin = 1; + LoopOverPoints(entities, constraints, faces, &pmax, &pmin, &wmin, + /*usePerspective=*/true, /*includeMesh=*/!selectionUsed, + camera); + + // Adjust the scale so that no points are behind the camera + if(wmin < 0.1) { + double k = camera.tangent; + // w = 1+k*scale*z + double zmin = (wmin - 1)/(k*scale); + // 0.1 = 1 + k*scale*zmin + // (0.1 - 1)/(k*zmin) = scale + scale = min(scale, (0.1 - 1)/(k*zmin)); + } + + return scale; +} + + +void GraphicsWindow::ZoomToMouse(double zoomMultiplyer) { + double offsetRight = offset.Dot(projRight); + double offsetUp = offset.Dot(projUp); + + double width, height; + window->GetContentSize(&width, &height); + + double righti = currentMousePosition.x / scale - offsetRight; + double upi = currentMousePosition.y / scale - offsetUp; + + // zoomMultiplyer of 1 gives a default zoom factor of 1.2x: zoomMultiplyer * 1.2 + // zoom = adjusted zoom negative zoomMultiplyer will zoom out, positive will zoom in + // + + scale *= exp(0.1823216 * zoomMultiplyer); // ln(1.2) = 0.1823216 + + double rightf = currentMousePosition.x / scale - offsetRight; + double upf = currentMousePosition.y / scale - offsetUp; + + offset = offset.Plus(projRight.ScaledBy(rightf - righti)); + offset = offset.Plus(projUp.ScaledBy(upf - upi)); + + if(SS.TW.shown.screen == TextWindow::Screen::EDIT_VIEW) { + if(havePainted) { + SS.ScheduleShowTW(); + } + } + havePainted = false; + Invalidate(); +} + + +void GraphicsWindow::MenuView(Command id) { + switch(id) { + case Command::ZOOM_IN: + SS.GW.ZoomToMouse(1); + break; + + case Command::ZOOM_OUT: + SS.GW.ZoomToMouse(-1); + break; + + case Command::ZOOM_TO_FIT: + SS.GW.ZoomToFit(/*includingInvisibles=*/false, /*useSelection=*/true); + SS.ScheduleShowTW(); + break; + + case Command::SHOW_GRID: + SS.GW.showSnapGrid = !SS.GW.showSnapGrid; + SS.GW.EnsureValidActives(); + SS.GW.Invalidate(); + if(SS.GW.showSnapGrid && !SS.GW.LockedInWorkplane()) { + Message(_("No workplane is active, so the grid will not appear.")); + } + break; + + case Command::DIM_SOLID_MODEL: + SS.GW.dimSolidModel = !SS.GW.dimSolidModel; + SS.GW.EnsureValidActives(); + SS.GW.Invalidate(/*clearPersistent=*/true); + break; + + case Command::PERSPECTIVE_PROJ: + SS.usePerspectiveProj = !SS.usePerspectiveProj; + SS.GW.EnsureValidActives(); + SS.GW.Invalidate(); + if(SS.cameraTangent < 1e-6) { + Error(_("The perspective factor is set to zero, so the view will " + "always be a parallel projection.\n\n" + "For a perspective projection, modify the perspective " + "factor in the configuration screen. A value around 0.3 " + "is typical.")); + } + break; + + case Command::EXPLODE_SKETCH: + SS.explode = !SS.explode; + SS.GW.EnsureValidActives(); + SS.MarkGroupDirty(SS.GW.activeGroup, true); + break; + + case Command::ONTO_WORKPLANE: + if(SS.GW.LockedInWorkplane()) { + SS.GW.AnimateOntoWorkplane(); + break; + } // if not in 2d mode use ORTHO logic + // fallthrough + case Command::NEAREST_ORTHO: + case Command::NEAREST_ISO: { + static const Vector ortho[3] = { + Vector::From(1, 0, 0), + Vector::From(0, 1, 0), + Vector::From(0, 0, 1) + }; + double sqrt2 = sqrt(2.0), sqrt6 = sqrt(6.0); + Quaternion quat0 = Quaternion::From(SS.GW.projRight, SS.GW.projUp); + Quaternion quatf = quat0; + double dmin = 1e10; + + // There are 24 possible views (3*2*2*2), if all are + // allowed. If the user is in turn-table mode, the + // isometric view must have the z-axis facing up, leaving + // 8 possible views (2*1*2*2). + + bool require_turntable = (id==Command::NEAREST_ISO && SS.turntableNav); + for(int i = 0; i < 3; i++) { + for(int j = 0; j < 3; j++) { + if(i == j) continue; + if(require_turntable && (j!=2)) continue; + for(int negi = 0; negi < 2; negi++) { + for(int negj = 0; negj < 2; negj++) { + Vector ou = ortho[i], ov = ortho[j]; + if(negi) ou = ou.ScaledBy(-1); + if(negj) ov = ov.ScaledBy(-1); + Vector on = ou.Cross(ov); + + Vector u, v; + if(id == Command::NEAREST_ORTHO || id == Command::ONTO_WORKPLANE) { + u = ou; + v = ov; + } else { + u = + ou.ScaledBy(1/sqrt2).Plus( + on.ScaledBy(-1/sqrt2)); + v = + ou.ScaledBy(-1/sqrt6).Plus( + ov.ScaledBy(2/sqrt6).Plus( + on.ScaledBy(-1/sqrt6))); + } + + Quaternion quatt = Quaternion::From(u, v); + double d = min( + (quatt.Minus(quat0)).Magnitude(), + (quatt.Plus(quat0)).Magnitude()); + if(d < dmin) { + dmin = d; + quatf = quatt; + } + } + } + } + } + + SS.GW.AnimateOnto(quatf, SS.GW.offset); + break; + } + + case Command::CENTER_VIEW: + SS.GW.GroupSelection(); + if(SS.GW.gs.n == 1 && SS.GW.gs.points == 1) { + Quaternion quat0; + // Offset is the selected point, quaternion is same as before + Vector pt = SK.GetEntity(SS.GW.gs.point[0])->PointGetNum(); + quat0 = Quaternion::From(SS.GW.projRight, SS.GW.projUp); + SS.GW.ClearSelection(); + SS.GW.AnimateOnto(quat0, pt.ScaledBy(-1)); + } else { + Error(_("Select a point; this point will become the center " + "of the view on screen.")); + } + break; + + case Command::SHOW_TOOLBAR: + SS.showToolbar = !SS.showToolbar; + SS.GW.EnsureValidActives(); + SS.GW.Invalidate(); + break; + + case Command::SHOW_TEXT_WND: + SS.GW.showTextWindow = !SS.GW.showTextWindow; + SS.GW.EnsureValidActives(); + break; + + case Command::UNITS_INCHES: + SS.viewUnits = Unit::INCHES; + SS.ScheduleShowTW(); + SS.GW.EnsureValidActives(); + break; + + case Command::UNITS_FEET_INCHES: + SS.viewUnits = Unit::FEET_INCHES; + SS.ScheduleShowTW(); + SS.GW.EnsureValidActives(); + break; + + case Command::UNITS_MM: + SS.viewUnits = Unit::MM; + SS.ScheduleShowTW(); + SS.GW.EnsureValidActives(); + break; + + case Command::UNITS_METERS: + SS.viewUnits = Unit::METERS; + SS.ScheduleShowTW(); + SS.GW.EnsureValidActives(); + break; + + case Command::FULL_SCREEN: + SS.GW.window->SetFullScreen(!SS.GW.window->IsFullScreen()); + SS.GW.EnsureValidActives(); + break; + + default: ssassert(false, "Unexpected menu ID"); + } + SS.GW.Invalidate(); +} + +void GraphicsWindow::EnsureValidActives() { + bool change = false; + // The active group must exist, and not be the references. + Group *g = SK.group.FindByIdNoOops(activeGroup); + if((!g) || (g->h == Group::HGROUP_REFERENCES)) { + // Not using range-for because this is used to find an index. + int i; + for(i = 0; i < SK.groupOrder.n; i++) { + if(SK.groupOrder[i] != Group::HGROUP_REFERENCES) { + break; + } + } + if(i >= SK.groupOrder.n) { + // This can happen if the user deletes all the groups in the + // sketch. It's difficult to prevent that, because the last + // group might have been deleted automatically, because it failed + // a dependency. There needs to be something, so create a plane + // drawing group and activate that. They should never be able + // to delete the references, though. + activeGroup = SS.CreateDefaultDrawingGroup(); + // We've created the default group, but not the workplane entity; + // do it now so that drawing mode isn't switched to "Free in 3d". + SS.GenerateAll(SolveSpaceUI::Generate::ALL); + } else { + activeGroup = SK.groupOrder[i]; + } + SK.GetGroup(activeGroup)->Activate(); + change = true; + } + + // The active coordinate system must also exist. + if(LockedInWorkplane()) { + Entity *e = SK.entity.FindByIdNoOops(ActiveWorkplane()); + if(e) { + hGroup hgw = e->group; + if(hgw != activeGroup && SS.GroupsInOrder(activeGroup, hgw)) { + // The active workplane is in a group that comes after the + // active group; so any request or constraint will fail. + SetWorkplaneFreeIn3d(); + change = true; + } + } else { + SetWorkplaneFreeIn3d(); + change = true; + } + } + + if(!window) return; + + // And update the checked state for various menus + bool locked = LockedInWorkplane(); + in3dMenuItem->SetActive(!locked); + inWorkplaneMenuItem->SetActive(locked); + + SS.UndoEnableMenus(); + + switch(SS.viewUnits) { + case Unit::MM: + case Unit::METERS: + case Unit::INCHES: + case Unit::FEET_INCHES: + break; + default: + SS.viewUnits = Unit::MM; + break; + } + unitsMmMenuItem->SetActive(SS.viewUnits == Unit::MM); + unitsMetersMenuItem->SetActive(SS.viewUnits == Unit::METERS); + unitsInchesMenuItem->SetActive(SS.viewUnits == Unit::INCHES); + unitsFeetInchesMenuItem->SetActive(SS.viewUnits == Unit::FEET_INCHES); + + if(SS.TW.window) SS.TW.window->SetVisible(SS.GW.showTextWindow); + showTextWndMenuItem->SetActive(SS.GW.showTextWindow); + + showGridMenuItem->SetActive(SS.GW.showSnapGrid); + dimSolidModelMenuItem->SetActive(SS.GW.dimSolidModel); + perspectiveProjMenuItem->SetActive(SS.usePerspectiveProj); + explodeMenuItem->SetActive(SS.explode); + showToolbarMenuItem->SetActive(SS.showToolbar); + fullScreenMenuItem->SetActive(SS.GW.window->IsFullScreen()); + + if(change) SS.ScheduleShowTW(); +} + +void GraphicsWindow::SetWorkplaneFreeIn3d() { + SK.GetGroup(activeGroup)->activeWorkplane = Entity::FREE_IN_3D; +} +hEntity GraphicsWindow::ActiveWorkplane() { + Group *g = SK.group.FindByIdNoOops(activeGroup); + if(g) { + return g->activeWorkplane; + } else { + return Entity::FREE_IN_3D; + } +} +bool GraphicsWindow::LockedInWorkplane() { + return (SS.GW.ActiveWorkplane() != Entity::FREE_IN_3D); +} + +void GraphicsWindow::ForceTextWindowShown() { + if(!showTextWindow) { + showTextWindow = true; + showTextWndMenuItem->SetActive(true); + SS.TW.window->SetVisible(true); + } +} + +void GraphicsWindow::DeleteTaggedRequests() { + // Rewrite any point-coincident constraints that were affected by this + // deletion. + for(Request &r : SK.request) { + if(!r.tag) continue; + FixConstraintsForRequestBeingDeleted(r.h); + } + // and then delete the tagged requests. + SK.request.RemoveTagged(); + + // An edit might be in progress for the just-deleted item. So + // now it's not. + window->HideEditor(); + SS.TW.HideEditControl(); + // And clear out the selection, which could contain that item. + ClearSuper(); + // And regenerate to get rid of what it generates, plus anything + // that references it (since the regen code checks for that). + SS.GenerateAll(SolveSpaceUI::Generate::ALL); + EnsureValidActives(); + SS.ScheduleShowTW(); +} + +Vector GraphicsWindow::SnapToGrid(Vector p) { + if(!LockedInWorkplane()) return p; + + EntityBase *wrkpl = SK.GetEntity(ActiveWorkplane()), + *norm = wrkpl->Normal(); + Vector wo = SK.GetEntity(wrkpl->point[0])->PointGetNum(), + wu = norm->NormalU(), + wv = norm->NormalV(), + wn = norm->NormalN(); + + Vector pp = (p.Minus(wo)).DotInToCsys(wu, wv, wn); + pp.x = floor((pp.x / SS.gridSpacing) + 0.5)*SS.gridSpacing; + pp.y = floor((pp.y / SS.gridSpacing) + 0.5)*SS.gridSpacing; + pp.z = 0; + + return pp.ScaleOutOfCsys(wu, wv, wn).Plus(wo); +} + +void GraphicsWindow::MenuEdit(Command id) { + switch(id) { + case Command::UNSELECT_ALL: + SS.GW.GroupSelection(); + // If there's nothing selected to de-select, and no operation + // to cancel, then perhaps they want to return to the home + // screen in the text window. + if(SS.GW.gs.n == 0 && + SS.GW.gs.constraints == 0 && + SS.GW.pending.operation == Pending::NONE) + { + if(!(SS.TW.window->IsEditorVisible() || + SS.GW.window->IsEditorVisible())) + { + if(SS.TW.shown.screen == TextWindow::Screen::STYLE_INFO) { + SS.TW.GoToScreen(TextWindow::Screen::LIST_OF_STYLES); + } else { + SS.TW.ClearSuper(); + } + } + } + // some pending operations need an Undo to properly clean up on ESC + if ( (SS.GW.pending.operation == Pending::DRAGGING_NEW_POINT) + || (SS.GW.pending.operation == Pending::DRAGGING_NEW_LINE_POINT) + || (SS.GW.pending.operation == Pending::DRAGGING_NEW_ARC_POINT) + || (SS.GW.pending.operation == Pending::DRAGGING_NEW_RADIUS) ) + { + SS.GW.ClearSuper(); + SS.UndoUndo(); + } + SS.GW.ClearSuper(); + SS.TW.HideEditControl(); + SS.nakedEdges.Clear(); + SS.justExportedInfo.draw = false; + SS.centerOfMass.draw = false; + // This clears the marks drawn to indicate which points are + // still free to drag. + for(Param &p : SK.param) { + p.free = false; + } + if(SS.exportMode) { + SS.exportMode = false; + SS.GenerateAll(SolveSpaceUI::Generate::ALL); + } + SS.GW.persistentDirty = true; + break; + + case Command::SELECT_ALL: { + for(Entity &e : SK.entity) { + if(e.group != SS.GW.activeGroup) continue; + if(e.IsFace() || e.IsDistance()) continue; + if(!e.IsVisible()) continue; + + SS.GW.MakeSelected(e.h); + } + SS.GW.Invalidate(); + SS.ScheduleShowTW(); + break; + } + + case Command::SELECT_CHAIN: { + int newlySelected = 0; + bool didSomething; + do { + didSomething = false; + for(Entity &e : SK.entity) { + if(e.group != SS.GW.activeGroup) continue; + if(!e.HasEndpoints()) continue; + if(!e.IsVisible()) continue; + + Vector st = e.EndpointStart(), + fi = e.EndpointFinish(); + + bool onChain = false, alreadySelected = false; + List *ls = &(SS.GW.selection); + for(Selection *s = ls->First(); s; s = ls->NextAfter(s)) { + if(!s->entity.v) continue; + if(s->entity == e.h) { + alreadySelected = true; + continue; + } + Entity *se = SK.GetEntity(s->entity); + if(!se->HasEndpoints()) continue; + + Vector sst = se->EndpointStart(), + sfi = se->EndpointFinish(); + + if(sst.Equals(st) || sst.Equals(fi) || + sfi.Equals(st) || sfi.Equals(fi)) + { + onChain = true; + } + } + if(onChain && !alreadySelected) { + SS.GW.MakeSelected(e.h); + newlySelected++; + didSomething = true; + } + } + } while(didSomething); + SS.GW.Invalidate(); + SS.ScheduleShowTW(); + if(newlySelected == 0) { + Error(_("No additional entities share endpoints with the selected entities.")); + } + break; + } + + case Command::ROTATE_90: { + SS.GW.GroupSelection(); + Entity *e = NULL; + if(SS.GW.gs.n == 1 && SS.GW.gs.points == 1) { + e = SK.GetEntity(SS.GW.gs.point[0]); + } else if(SS.GW.gs.n == 1 && SS.GW.gs.entities == 1) { + e = SK.GetEntity(SS.GW.gs.entity[0]); + } + SS.GW.ClearSelection(); + + hGroup hg = e ? e->group : SS.GW.activeGroup; + Group *g = SK.GetGroup(hg); + if(g->type != Group::Type::LINKED) { + Error(_("To use this command, select a point or other " + "entity from an linked part, or make a link " + "group the active group.")); + break; + } + + SS.UndoRemember(); + // Rotate by ninety degrees about the coordinate axis closest + // to the screen normal. + Vector norm = SS.GW.projRight.Cross(SS.GW.projUp); + norm = norm.ClosestOrtho(); + norm = norm.WithMagnitude(1); + Quaternion qaa = Quaternion::From(norm, PI/2); + + g->TransformImportedBy(Vector::From(0, 0, 0), qaa); + + // and regenerate as necessary. + SS.MarkGroupDirty(hg); + break; + } + + case Command::SNAP_TO_GRID: { + if(!SS.GW.LockedInWorkplane()) { + Error(_("No workplane is active. Activate a workplane " + "(with Sketch -> In Workplane) to define the plane " + "for the snap grid.")); + break; + } + SS.GW.GroupSelection(); + if(SS.GW.gs.points == 0 && SS.GW.gs.constraintLabels == 0) { + Error(_("Can't snap these items to grid; select points, " + "text comments, or constraints with a label. " + "To snap a line, select its endpoints.")); + break; + } + SS.UndoRemember(); + + List *ls = &(SS.GW.selection); + for(Selection *s = ls->First(); s; s = ls->NextAfter(s)) { + if(s->entity.v) { + hEntity hp = s->entity; + Entity *ep = SK.GetEntity(hp); + if(!ep->IsPoint()) continue; + + Vector p = ep->PointGetNum(); + ep->PointForceTo(SS.GW.SnapToGrid(p)); + SS.GW.pending.points.Add(&hp); + SS.MarkGroupDirty(ep->group); + } else if(s->constraint.v) { + Constraint *c = SK.GetConstraint(s->constraint); + std::vector refs; + c->GetReferencePoints(SS.GW.GetCamera(), &refs); + c->disp.offset = c->disp.offset.Plus(SS.GW.SnapToGrid(refs[0]).Minus(refs[0])); + } + } + // Regenerate, with these points marked as dragged so that they + // get placed as close as possible to our snap grid. + SS.GW.ClearSelection(); + break; + } + + case Command::UNDO: + SS.UndoUndo(); + break; + + case Command::REDO: + SS.UndoRedo(); + break; + + case Command::REGEN_ALL: + SS.images.clear(); + SS.ReloadAllLinked(SS.saveFile); + SS.GenerateAll(SolveSpaceUI::Generate::UNTIL_ACTIVE); + SS.ScheduleShowTW(); + break; + + case Command::EDIT_LINE_STYLES: + SS.TW.GoToScreen(TextWindow::Screen::LIST_OF_STYLES); + SS.GW.ForceTextWindowShown(); + SS.ScheduleShowTW(); + break; + case Command::VIEW_PROJECTION: + SS.TW.GoToScreen(TextWindow::Screen::EDIT_VIEW); + SS.GW.ForceTextWindowShown(); + SS.ScheduleShowTW(); + break; + case Command::CONFIGURATION: + SS.TW.GoToScreen(TextWindow::Screen::CONFIGURATION); + SS.GW.ForceTextWindowShown(); + SS.ScheduleShowTW(); + break; + + default: ssassert(false, "Unexpected menu ID"); + } +} + +void GraphicsWindow::MenuRequest(Command id) { + const char *s; + switch(id) { + case Command::SEL_WORKPLANE: { + SS.GW.GroupSelection(); + Group *g = SK.GetGroup(SS.GW.activeGroup); + + if(SS.GW.gs.n == 1 && SS.GW.gs.workplanes == 1) { + // A user-selected workplane + g->activeWorkplane = SS.GW.gs.entity[0]; + SS.GW.EnsureValidActives(); + SS.ScheduleShowTW(); + } else if(g->type == Group::Type::DRAWING_WORKPLANE) { + // The group's default workplane + g->activeWorkplane = g->h.entity(0); + MessageAndRun([] { + // Align the view with the selected workplane + SS.GW.ClearSuper(); + SS.GW.AnimateOntoWorkplane(); + }, _("No workplane selected. Activating default workplane " + "for this group.")); + } else { + Error(_("No workplane is selected, and the active group does " + "not have a default workplane. Try selecting a " + "workplane, or activating a sketch-in-new-workplane " + "group.")); + //update checkboxes in the menus + SS.GW.EnsureValidActives(); + } + break; + } + case Command::FREE_IN_3D: + SS.GW.SetWorkplaneFreeIn3d(); + SS.GW.EnsureValidActives(); + SS.ScheduleShowTW(); + SS.GW.Invalidate(); + break; + + case Command::TANGENT_ARC: + SS.GW.GroupSelection(); + if(SS.GW.gs.n == 1 && SS.GW.gs.points == 1) { + SS.GW.MakeTangentArc(); + } else if(SS.GW.gs.n != 0) { + Error(_("Bad selection for tangent arc at point. Select a " + "single point, or select nothing to set up arc " + "parameters.")); + } else { + SS.TW.GoToScreen(TextWindow::Screen::TANGENT_ARC); + SS.GW.ForceTextWindowShown(); + SS.ScheduleShowTW(); + SS.GW.Invalidate(); // repaint toolbar + } + break; + + case Command::ARC: s = _("click point on arc (draws anti-clockwise)"); goto c; + case Command::DATUM_POINT: s = _("click to place datum point"); goto c; + case Command::LINE_SEGMENT: s = _("click first point of line segment"); goto c; + case Command::CONSTR_SEGMENT: + s = _("click first point of construction line segment"); goto c; + case Command::CUBIC: s = _("click first point of cubic segment"); goto c; + case Command::CIRCLE: s = _("click center of circle"); goto c; + case Command::WORKPLANE: s = _("click origin of workplane"); goto c; + case Command::RECTANGLE: s = _("click one corner of rectangle"); goto c; + case Command::TTF_TEXT: s = _("click top left of text"); goto c; + case Command::IMAGE: + if(!SS.ReloadLinkedImage(SS.saveFile, &SS.GW.pending.filename, + /*canCancel=*/true)) { + return; + } + s = _("click top left of image"); goto c; +c: + SS.GW.pending.operation = GraphicsWindow::Pending::COMMAND; + SS.GW.pending.command = id; + SS.GW.pending.description = s; + SS.ScheduleShowTW(); + SS.GW.Invalidate(); // repaint toolbar + break; + + case Command::CONSTRUCTION: { + // if we are drawing + if(SS.GW.pending.operation == Pending::DRAGGING_NEW_POINT || + SS.GW.pending.operation == Pending::DRAGGING_NEW_LINE_POINT || + SS.GW.pending.operation == Pending::DRAGGING_NEW_ARC_POINT || + SS.GW.pending.operation == Pending::DRAGGING_NEW_CUBIC_POINT || + SS.GW.pending.operation == Pending::DRAGGING_NEW_RADIUS) { + for(auto &hr : SS.GW.pending.requests) { + Request* r = SK.GetRequest(hr); + r->construction = !(r->construction); + SS.MarkGroupDirty(r->group); + } + SS.GW.Invalidate(); + break; + } + SS.GW.GroupSelection(); + if(SS.GW.gs.entities == 0) { + Error(_("No entities are selected. Select entities before " + "trying to toggle their construction state.")); + break; + } + SS.UndoRemember(); + int i; + for(i = 0; i < SS.GW.gs.entities; i++) { + hEntity he = SS.GW.gs.entity[i]; + if(!he.isFromRequest()) continue; + Request *r = SK.GetRequest(he.request()); + r->construction = !(r->construction); + SS.MarkGroupDirty(r->group); + } + SS.GW.ClearSelection(); + break; + } + + case Command::SPLIT_CURVES: + SS.GW.SplitLinesOrCurves(); + break; + + default: ssassert(false, "Unexpected menu ID"); + } +} + +void GraphicsWindow::ClearSuper() { + if(window) window->HideEditor(); + ClearPending(); + ClearSelection(); + hover.Clear(); + EnsureValidActives(); +} + +void GraphicsWindow::ToggleBool(bool *v) { + *v = !*v; + + // The faces are shown as special stippling on the shaded triangle mesh, + // so not meaningful to show them and hide the shaded. + if(!showShaded) showFaces = false; + + // If the mesh or edges were previously hidden, they haven't been generated, + // and if we are going to show them, we need to generate them first. + Group *g = SK.GetGroup(SS.GW.activeGroup); + if(*v && (g->displayOutlines.l.IsEmpty() && (v == &showEdges || v == &showOutlines))) { + SS.GenerateAll(SolveSpaceUI::Generate::UNTIL_ACTIVE); + } + + if(v == &showFaces) { + if(g->type == Group::Type::DRAWING_WORKPLANE || g->type == Group::Type::DRAWING_3D) { + showFacesDrawing = showFaces; + } else { + showFacesNonDrawing = showFaces; + } + } + + Invalidate(/*clearPersistent=*/true); + SS.ScheduleShowTW(); +} + +bool GraphicsWindow::SuggestLineConstraint(hRequest request, Constraint::Type *type) { + if(!(LockedInWorkplane() && SS.automaticLineConstraints)) + return false; + + Entity *ptA = SK.GetEntity(request.entity(1)), + *ptB = SK.GetEntity(request.entity(2)); + + Expr *au, *av, *bu, *bv; + + ptA->PointGetExprsInWorkplane(ActiveWorkplane(), &au, &av); + ptB->PointGetExprsInWorkplane(ActiveWorkplane(), &bu, &bv); + + double du = au->Minus(bu)->Eval(); + double dv = av->Minus(bv)->Eval(); + + const double TOLERANCE_RATIO = 0.02; + if(fabs(dv) > LENGTH_EPS && fabs(du / dv) < TOLERANCE_RATIO) { + *type = Constraint::Type::VERTICAL; + return true; + } else if(fabs(du) > LENGTH_EPS && fabs(dv / du) < TOLERANCE_RATIO) { + *type = Constraint::Type::HORIZONTAL; + return true; + } + return false; +} + +} // namespace SolveSpace diff --git a/src/group.cpp b/src/group.cpp new file mode 100644 index 0000000..0b9cd2f --- /dev/null +++ b/src/group.cpp @@ -0,0 +1,1227 @@ +//----------------------------------------------------------------------------- +// Implementation of the Group class, which represents a set of entities and +// constraints that are solved together, in some cases followed by another +// operation, like to extrude surfaces from the entities or to step and +// repeat them parametrically. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +const hParam Param::NO_PARAM = { 0 }; +#define NO_PARAM (Param::NO_PARAM) + +const hGroup Group::HGROUP_REFERENCES = { 1 }; + +//----------------------------------------------------------------------------- +// The group structure includes pointers to other dynamically-allocated +// memory. This clears and frees them all. +//----------------------------------------------------------------------------- +void Group::Clear() { + polyLoops.Clear(); + bezierLoops.Clear(); + bezierOpens.Clear(); + thisMesh.Clear(); + runningMesh.Clear(); + thisShell.Clear(); + runningShell.Clear(); + displayMesh.Clear(); + displayOutlines.Clear(); + impMesh.Clear(); + impShell.Clear(); + impEntity.Clear(); + // remap is the only one that doesn't get recreated when we regen + remap.clear(); +} + +void Group::AddParam(ParamList *param, hParam hp, double v) { + Param pa = {}; + pa.h = hp; + pa.val = v; + + param->Add(&pa); +} + +bool Group::IsVisible() { + if(!visible) return false; + Group *active = SK.GetGroup(SS.GW.activeGroup); + if(order > active->order) return false; + return true; +} + +size_t Group::GetNumConstraints() { + return std::count_if(SK.constraint.begin(), SK.constraint.end(), + [&](Constraint const &c) { return c.group == h; }); +} + +Vector Group::ExtrusionGetVector() { + return Vector::From(h.param(0), h.param(1), h.param(2)); +} + +void Group::ExtrusionForceVectorTo(const Vector &v) { + SK.GetParam(h.param(0))->val = v.x; + SK.GetParam(h.param(1))->val = v.y; + SK.GetParam(h.param(2))->val = v.z; +} + +void Group::MenuGroup(Command id) { + MenuGroup(id, Platform::Path()); +} + +void Group::MenuGroup(Command id, Platform::Path linkFile) { + Platform::SettingsRef settings = Platform::GetSettings(); + + Group g = {}; + g.visible = true; + g.color = RGBi(100, 100, 100); + g.scale = 1; + g.linkFile = linkFile; + + SS.GW.GroupSelection(); + auto const &gs = SS.GW.gs; + + switch(id) { + case Command::GROUP_3D: + g.type = Type::DRAWING_3D; + g.name = C_("group-name", "sketch-in-3d"); + break; + + case Command::GROUP_WRKPL: + g.type = Type::DRAWING_WORKPLANE; + g.name = C_("group-name", "sketch-in-plane"); + if(gs.points == 1 && gs.n == 1) { + g.subtype = Subtype::WORKPLANE_BY_POINT_ORTHO; + + Vector u = SS.GW.projRight, v = SS.GW.projUp; + u = u.ClosestOrtho(); + v = v.Minus(u.ScaledBy(v.Dot(u))); + v = v.ClosestOrtho(); + + g.predef.q = Quaternion::From(u, v); + g.predef.origin = gs.point[0]; + } else if(gs.points == 1 && gs.lineSegments == 2 && gs.n == 3) { + g.subtype = Subtype::WORKPLANE_BY_LINE_SEGMENTS; + + g.predef.origin = gs.point[0]; + g.predef.entityB = gs.entity[0]; + g.predef.entityC = gs.entity[1]; + + Vector ut = SK.GetEntity(g.predef.entityB)->VectorGetNum(); + Vector vt = SK.GetEntity(g.predef.entityC)->VectorGetNum(); + ut = ut.WithMagnitude(1); + vt = vt.WithMagnitude(1); + + if(fabs(SS.GW.projUp.Dot(vt)) < fabs(SS.GW.projUp.Dot(ut))) { + swap(ut, vt); + g.predef.swapUV = true; + } + if(SS.GW.projRight.Dot(ut) < 0) g.predef.negateU = true; + if(SS.GW.projUp. Dot(vt) < 0) g.predef.negateV = true; + } else if(gs.workplanes == 1 && gs.n == 1) { + if(gs.entity[0].isFromRequest()) { + Entity *wrkpl = SK.GetEntity(gs.entity[0]); + Entity *normal = SK.GetEntity(wrkpl->normal); + g.subtype = Subtype::WORKPLANE_BY_POINT_ORTHO; + g.predef.origin = wrkpl->point[0]; + g.predef.q = normal->NormalGetNum(); + } else { + Group *wrkplg = SK.GetGroup(gs.entity[0].group()); + g.subtype = wrkplg->subtype; + g.predef.origin = wrkplg->predef.origin; + if(wrkplg->subtype == Subtype::WORKPLANE_BY_LINE_SEGMENTS) { + g.predef.entityB = wrkplg->predef.entityB; + g.predef.entityC = wrkplg->predef.entityC; + g.predef.swapUV = wrkplg->predef.swapUV; + g.predef.negateU = wrkplg->predef.negateU; + g.predef.negateV = wrkplg->predef.negateV; + } else if(wrkplg->subtype == Subtype::WORKPLANE_BY_POINT_ORTHO) { + g.predef.q = wrkplg->predef.q; + } else if(wrkplg->subtype == Subtype::WORKPLANE_BY_POINT_NORMAL) { + g.predef.q = wrkplg->predef.q; + g.predef.entityB = wrkplg->predef.entityB; + } else ssassert(false, "Unexpected workplane subtype"); + } + } else if(gs.anyNormals == 1 && gs.points == 1 && gs.n == 2) { + g.subtype = Subtype::WORKPLANE_BY_POINT_NORMAL; + g.predef.entityB = gs.anyNormal[0]; + g.predef.q = SK.GetEntity(gs.anyNormal[0])->NormalGetNum(); + g.predef.origin = gs.point[0]; + //} else if(gs.faces == 1 && gs.points == 1 && gs.n == 2) { + // g.subtype = Subtype::WORKPLANE_BY_POINT_FACE; + // g.predef.q = SK.GetEntity(gs.face[0])->NormalGetNum(); + // g.predef.origin = gs.point[0]; + } else { + Error(_("Bad selection for new sketch in workplane. This " + "group can be created with:\n\n" + " * a point (through the point, orthogonal to coordinate axes)\n" + " * a point and two line segments (through the point, " + "parallel to the lines)\n" + " * a point and a normal (through the point, " + "orthogonal to the normal)\n" + /*" * a point and a face (through the point, " + "parallel to the face)\n"*/ + " * a workplane (copy of the workplane)\n")); + return; + } + break; + + case Command::GROUP_EXTRUDE: + if(!SS.GW.LockedInWorkplane()) { + Error(_("Activate a workplane (Sketch -> In Workplane) before " + "extruding. The sketch will be extruded normal to the " + "workplane.")); + return; + } + g.type = Type::EXTRUDE; + g.opA = SS.GW.activeGroup; + g.predef.entityB = SS.GW.ActiveWorkplane(); + g.subtype = Subtype::ONE_SIDED; + g.name = C_("group-name", "extrude"); + break; + + case Command::GROUP_LATHE: + if(!SS.GW.LockedInWorkplane()) { + Error(_("Lathe operation can only be applied to planar sketches.")); + return; + } + if(gs.points == 1 && gs.vectors == 1 && gs.n == 2) { + g.predef.origin = gs.point[0]; + g.predef.entityB = gs.vector[0]; + } else if(gs.lineSegments == 1 && gs.n == 1) { + g.predef.origin = SK.GetEntity(gs.entity[0])->point[0]; + g.predef.entityB = gs.entity[0]; + // since a line segment is a vector + } else { + Error(_("Bad selection for new lathe group. This group can " + "be created with:\n\n" + " * a point and a line segment or normal " + "(revolved about an axis parallel to line / " + "normal, through point)\n" + " * a line segment (revolved about line segment)\n")); + return; + } + g.type = Type::LATHE; + g.opA = SS.GW.activeGroup; + g.name = C_("group-name", "lathe"); + break; + + case Command::GROUP_REVOLVE: + if(!SS.GW.LockedInWorkplane()) { + Error(_("Revolve operation can only be applied to planar sketches.")); + return; + } + if(gs.points == 1 && gs.vectors == 1 && gs.n == 2) { + g.predef.origin = gs.point[0]; + g.predef.entityB = gs.vector[0]; + } else if(gs.lineSegments == 1 && gs.n == 1) { + g.predef.origin = SK.GetEntity(gs.entity[0])->point[0]; + g.predef.entityB = gs.entity[0]; + // since a line segment is a vector + } else { + Error(_("Bad selection for new revolve group. This group can " + "be created with:\n\n" + " * a point and a line segment or normal " + "(revolved about an axis parallel to line / " + "normal, through point)\n" + " * a line segment (revolved about line segment)\n")); + return; + } + g.type = Type::REVOLVE; + g.opA = SS.GW.activeGroup; + g.valA = 2; + g.subtype = Subtype::ONE_SIDED; + g.name = C_("group-name", "revolve"); + break; + + case Command::GROUP_HELIX: + if(!SS.GW.LockedInWorkplane()) { + Error(_("Helix operation can only be applied to planar sketches.")); + return; + } + if(gs.points == 1 && gs.vectors == 1 && gs.n == 2) { + g.predef.origin = gs.point[0]; + g.predef.entityB = gs.vector[0]; + } else if(gs.lineSegments == 1 && gs.n == 1) { + g.predef.origin = SK.GetEntity(gs.entity[0])->point[0]; + g.predef.entityB = gs.entity[0]; + // since a line segment is a vector + } else { + Error(_("Bad selection for new helix group. This group can " + "be created with:\n\n" + " * a point and a line segment or normal " + "(revolved about an axis parallel to line / " + "normal, through point)\n" + " * a line segment (revolved about line segment)\n")); + return; + } + g.type = Type::HELIX; + g.opA = SS.GW.activeGroup; + g.valA = 2; + g.subtype = Subtype::ONE_SIDED; + g.name = C_("group-name", "helix"); + break; + + case Command::GROUP_ROT: { + if(gs.points == 1 && gs.n == 1 && SS.GW.LockedInWorkplane()) { + g.predef.origin = gs.point[0]; + Entity *w = SK.GetEntity(SS.GW.ActiveWorkplane()); + g.predef.entityB = w->Normal()->h; + g.activeWorkplane = w->h; + } else if(gs.points == 1 && gs.vectors == 1 && gs.n == 2) { + g.predef.origin = gs.point[0]; + g.predef.entityB = gs.vector[0]; + } else { + Error(_("Bad selection for new rotation. This group can " + "be created with:\n\n" + " * a point, while locked in workplane (rotate " + "in plane, about that point)\n" + " * a point and a line or a normal (rotate about " + "an axis through the point, and parallel to " + "line / normal)\n")); + return; + } + g.type = Type::ROTATE; + g.opA = SS.GW.activeGroup; + g.valA = 3; + g.subtype = Subtype::ONE_SIDED; + g.name = C_("group-name", "rotate"); + break; + } + + case Command::GROUP_TRANS: + g.type = Type::TRANSLATE; + g.opA = SS.GW.activeGroup; + g.valA = 3; + g.subtype = Subtype::ONE_SIDED; + g.predef.entityB = SS.GW.ActiveWorkplane(); + g.activeWorkplane = SS.GW.ActiveWorkplane(); + g.name = C_("group-name", "translate"); + break; + + case Command::GROUP_LINK: { + g.type = Type::LINKED; + g.meshCombine = CombineAs::ASSEMBLE; + if(g.linkFile.IsEmpty()) { + Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window); + dialog->AddFilters(Platform::SolveSpaceLinkFileFilters); + dialog->ThawChoices(settings, "LinkSketch"); + if(!dialog->RunModal()) return; + dialog->FreezeChoices(settings, "LinkSketch"); + g.linkFile = dialog->GetFilename(); + } + + // Assign the default name of the group based on the name of + // the linked file. + g.name = g.linkFile.FileStem(); + for(size_t i = 0; i < g.name.length(); i++) { + if(!(isalnum(g.name[i]) || (unsigned)g.name[i] >= 0x80)) { + // convert punctuation to dashes + g.name[i] = '-'; + } + } + break; + } + + default: ssassert(false, "Unexpected menu ID"); + } + + // Copy color from the previous mesh-contributing group. + if(g.IsMeshGroup() && !SK.groupOrder.IsEmpty()) { + Group *running = SK.GetRunningMeshGroupFor(SS.GW.activeGroup); + if(running != NULL) { + g.color = running->color; + } + } + + SS.GW.ClearSelection(); + SS.UndoRemember(); + + bool afterActive = false; + for(hGroup hg : SK.groupOrder) { + Group *gi = SK.GetGroup(hg); + if(afterActive) + gi->order += 1; + if(gi->h == SS.GW.activeGroup) { + g.order = gi->order + 1; + afterActive = true; + } + } + + SK.group.AddAndAssignId(&g); + Group *gg = SK.GetGroup(g.h); + + if(gg->type == Type::LINKED) { + SS.ReloadAllLinked(SS.saveFile); + } + gg->clean = false; + SS.GW.activeGroup = gg->h; + SS.GenerateAll(); + if(gg->type == Type::DRAWING_WORKPLANE) { + // Can't set the active workplane for this one until after we've + // regenerated, because the workplane doesn't exist until then. + gg->activeWorkplane = gg->h.entity(0); + } + gg->Activate(); + TextWindow::ScreenSelectGroup(0, gg->h.v); + SS.GW.AnimateOntoWorkplane(); +} + +void Group::TransformImportedBy(Vector t, Quaternion q) { + ssassert(type == Type::LINKED, "Expected a linked group"); + + hParam tx, ty, tz, qw, qx, qy, qz; + tx = h.param(0); + ty = h.param(1); + tz = h.param(2); + qw = h.param(3); + qx = h.param(4); + qy = h.param(5); + qz = h.param(6); + + Quaternion qg = Quaternion::From(qw, qx, qy, qz); + qg = q.Times(qg); + + Vector tg = Vector::From(tx, ty, tz); + tg = tg.Plus(t); + + SK.GetParam(tx)->val = tg.x; + SK.GetParam(ty)->val = tg.y; + SK.GetParam(tz)->val = tg.z; + + SK.GetParam(qw)->val = qg.w; + SK.GetParam(qx)->val = qg.vx; + SK.GetParam(qy)->val = qg.vy; + SK.GetParam(qz)->val = qg.vz; +} + +bool Group::IsForcedToMeshBySource() const { + const Group *srcg = this; + if(type == Type::TRANSLATE || type == Type::ROTATE) { + // A step and repeat gets merged against the group's previous group, + // not our own previous group. + srcg = SK.GetGroup(opA); + if(srcg->forceToMesh) return true; + } + Group *g = srcg->RunningMeshGroup(); + if(g == NULL) return false; + return g->forceToMesh || g->IsForcedToMeshBySource(); +} + +bool Group::IsForcedToMesh() const { + return forceToMesh || IsTriangleMeshAssembly() || IsForcedToMeshBySource(); +} + +bool Group::IsTriangleMeshAssembly() const { + if (type != Type::LINKED) return false; + if (!impMesh.IsEmpty() && impShell.IsEmpty()) return true; + return false; +} + +std::string Group::DescriptionString() { + if(name.empty()) { + return ssprintf("g%03x-%s", h.v, _("(unnamed)")); + } else { + return ssprintf("g%03x-%s", h.v, name.c_str()); + } +} + +void Group::Activate() { + if(type == Type::DRAWING_WORKPLANE || type == Type::DRAWING_3D) { + SS.GW.showFaces = SS.GW.showFacesDrawing; + } else { + SS.GW.showFaces = SS.GW.showFacesNonDrawing; + } + SS.MarkGroupDirty(h); // for good measure; shouldn't be needed + SS.ScheduleShowTW(); +} + +void Group::Generate(EntityList *entity, ParamList *param) +{ + Vector gn = (SS.GW.projRight).Cross(SS.GW.projUp); + Vector gp = SS.GW.projRight.Plus(SS.GW.projUp); + Vector gc = (SS.GW.offset).ScaledBy(-1); + gn = gn.WithMagnitude(200/SS.GW.scale); + gp = gp.WithMagnitude(200/SS.GW.scale); + int a, i; + switch(type) { + case Type::DRAWING_3D: + return; + + case Type::DRAWING_WORKPLANE: { + Quaternion q; + if(subtype == Subtype::WORKPLANE_BY_LINE_SEGMENTS) { + Vector u = SK.GetEntity(predef.entityB)->VectorGetNum(); + Vector v = SK.GetEntity(predef.entityC)->VectorGetNum(); + u = u.WithMagnitude(1); + Vector n = u.Cross(v); + v = (n.Cross(u)).WithMagnitude(1); + + if(predef.swapUV) swap(u, v); + if(predef.negateU) u = u.ScaledBy(-1); + if(predef.negateV) v = v.ScaledBy(-1); + q = Quaternion::From(u, v); + } else if(subtype == Subtype::WORKPLANE_BY_POINT_ORTHO) { + // Already given, numerically. + q = predef.q; + } else if(subtype == Subtype::WORKPLANE_BY_POINT_NORMAL) { + q = SK.GetEntity(predef.entityB)->NormalGetNum(); + } else ssassert(false, "Unexpected workplane subtype"); + + Entity normal = {}; + normal.type = Entity::Type::NORMAL_N_COPY; + normal.numNormal = q; + + normal.point[0] = h.entity(2); + normal.group = h; + normal.h = h.entity(1); + entity->Add(&normal); + + Entity point = {}; + point.type = Entity::Type::POINT_N_COPY; + point.numPoint = SK.GetEntity(predef.origin)->PointGetNum(); + point.construction = true; + point.group = h; + point.h = h.entity(2); + entity->Add(&point); + + Entity wp = {}; + wp.type = Entity::Type::WORKPLANE; + wp.normal = normal.h; + wp.point[0] = point.h; + wp.group = h; + wp.h = h.entity(0); + entity->Add(&wp); + return; + } + + case Type::EXTRUDE: { + AddParam(param, h.param(0), gn.x); + AddParam(param, h.param(1), gn.y); + AddParam(param, h.param(2), gn.z); + int ai, af; + if((subtype == Subtype::ONE_SIDED) || (subtype == Subtype::ONE_SKEWED)) { + ai = 0; af = 2; + } else if((subtype == Subtype::TWO_SIDED) || (subtype == Subtype::TWO_SKEWED)) { + ai = -1; af = 1; + } else ssassert(false, "Unexpected extrusion subtype"); + + // Get some arbitrary point in the sketch, that will be used + // as a reference when defining top and bottom faces. + hEntity pt = { 0 }; + // Not using range-for here because we're changing the size of entity in the loop. + for(i = 0; i < entity->n; i++) { + Entity *e = &(entity->Get(i)); + if(e->group != opA) continue; + + if(e->IsPoint()) pt = e->h; + + e->CalculateNumerical(/*forExport=*/false); + hEntity he = e->h; e = NULL; + // As soon as I call CopyEntity, e may become invalid! That + // adds entities, which may cause a realloc. + CopyEntity(entity, SK.GetEntity(he), ai, REMAP_BOTTOM, + h.param(0), h.param(1), h.param(2), + NO_PARAM, NO_PARAM, NO_PARAM, NO_PARAM, NO_PARAM, + CopyAs::N_TRANS); + CopyEntity(entity, SK.GetEntity(he), af, REMAP_TOP, + h.param(0), h.param(1), h.param(2), + NO_PARAM, NO_PARAM, NO_PARAM, NO_PARAM, NO_PARAM, + CopyAs::N_TRANS); + MakeExtrusionLines(entity, he); + } + // Remapped versions of that arbitrary point will be used to + // provide points on the plane faces. + MakeExtrusionTopBottomFaces(entity, pt); + return; + } + + case Type::LATHE: { + Vector axis_pos = SK.GetEntity(predef.origin)->PointGetNum(); + Vector axis_dir = SK.GetEntity(predef.entityB)->VectorGetNum(); + + // Not using range-for here because we're changing the size of entity in the loop. + for(i = 0; i < entity->n; i++) { + Entity *e = &(entity->Get(i)); + if(e->group != opA) continue; + + e->CalculateNumerical(/*forExport=*/false); + hEntity he = e->h; + + // As soon as I call CopyEntity, e may become invalid! That + // adds entities, which may cause a realloc. + + // this is the regular copy of all entities + CopyEntity(entity, SK.GetEntity(he), 0, REMAP_LATHE_START, + NO_PARAM, NO_PARAM, NO_PARAM, + NO_PARAM, NO_PARAM, NO_PARAM, NO_PARAM, NO_PARAM, + CopyAs::NUMERIC); + + e = &(entity->Get(i)); // because we copied. + if (e->IsPoint()) { + // for points this copy is used for the circle centers + CopyEntity(entity, SK.GetEntity(he), 0, REMAP_LATHE_ARC_CENTER, + NO_PARAM, NO_PARAM, NO_PARAM, + NO_PARAM, NO_PARAM, NO_PARAM, NO_PARAM, NO_PARAM, + CopyAs::NUMERIC); + MakeLatheCircles(entity, param, he, axis_pos, axis_dir); + }; + MakeLatheSurfacesSelectable(entity, he, axis_dir); + } + return; + } + + case Type::REVOLVE: { + // this was borrowed from LATHE and ROTATE + Vector axis_pos = SK.GetEntity(predef.origin)->PointGetNum(); + Vector axis_dir = SK.GetEntity(predef.entityB)->VectorGetNum(); + + // The center of rotation + AddParam(param, h.param(0), axis_pos.x); + AddParam(param, h.param(1), axis_pos.y); + AddParam(param, h.param(2), axis_pos.z); + // The rotation quaternion + AddParam(param, h.param(3), 30 * PI / 180); + AddParam(param, h.param(4), axis_dir.x); + AddParam(param, h.param(5), axis_dir.y); + AddParam(param, h.param(6), axis_dir.z); + + // Get some arbitrary point in the sketch, that will be used + // as a reference when defining end faces. + hEntity pt = { 0 }; + + int ai = 0, af = 2; + if (subtype == Subtype::TWO_SIDED) + { + ai = -1; + af = 1; + } + // Not using range-for here because we're changing the size of entity in the loop. + for(i = 0; i < entity->n; i++) { + Entity *e = &(entity->Get(i)); + if(e->group != opA) + continue; + + if(e->IsPoint()) pt = e->h; + + e->CalculateNumerical(/*forExport=*/false); + hEntity he = e->h; + // one copy for each end of the revolved surface + CopyEntity(entity, e, ai, REMAP_LATHE_START, h.param(0), + h.param(1), h.param(2), h.param(3), h.param(4), h.param(5), + h.param(6), NO_PARAM, CopyAs::N_ROT_AA); + + e = &(entity->Get(i)); // because we copied. + CopyEntity(entity, e, af, REMAP_LATHE_END, h.param(0), + h.param(1), h.param(2), h.param(3), h.param(4), h.param(5), + h.param(6), NO_PARAM, CopyAs::N_ROT_AA); + + // Arcs are not generated for revolve groups, for now, because our current arc + // entity is not chiral, and dragging a revolve may break the arc by inverting it. + // MakeLatheCircles(entity, param, he, axis_pos, axis_dir); + MakeLatheSurfacesSelectable(entity, he, axis_dir); + } + MakeRevolveEndFaces(entity, pt, ai, af); + return; + } + + case Type::HELIX: { + Vector axis_pos = SK.GetEntity(predef.origin)->PointGetNum(); + Vector axis_dir = SK.GetEntity(predef.entityB)->VectorGetNum(); + + // The center of rotation + AddParam(param, h.param(0), axis_pos.x); + AddParam(param, h.param(1), axis_pos.y); + AddParam(param, h.param(2), axis_pos.z); + // The rotation quaternion + AddParam(param, h.param(3), 30 * PI / 180); + AddParam(param, h.param(4), axis_dir.x); + AddParam(param, h.param(5), axis_dir.y); + AddParam(param, h.param(6), axis_dir.z); + // distance to translate along the rotation axis + AddParam(param, h.param(7), 20); + + // Get some arbitrary point in the sketch, that will be used + // as a reference when defining end faces. + hEntity pt = { 0 }; + + int ai = 0, af = 2; // initial and final number of transformations + if (subtype != Subtype::ONE_SIDED) + { + ai = -1; + af = 1; + } + + // Not using range-for here because we're changing the size of entity in the loop. + for(i = 0; i < entity->n; i++) { + Entity *e = &(entity->Get(i)); + if((e->group.v != opA.v) && !(e->h == predef.origin)) + continue; + + if(e->IsPoint()) pt = e->h; + + e->CalculateNumerical(/*forExport=*/false); + + // one copy for each end of the helix + CopyEntity(entity, e, ai, REMAP_LATHE_START, h.param(0), + h.param(1), h.param(2), h.param(3), h.param(4), h.param(5), + h.param(6), h.param(7), CopyAs::N_ROT_AXIS_TRANS); + + e = &(entity->Get(i)); // because we copied. + CopyEntity(entity, e, af, REMAP_LATHE_END, h.param(0), + h.param(1), h.param(2), h.param(3), h.param(4), h.param(5), + h.param(6), h.param(7), CopyAs::N_ROT_AXIS_TRANS); + + // For point entities on the axis, create a construction line + e = &(entity->Get(i)); + if(e->IsPoint()) { + Vector check = e->PointGetNum().Minus(axis_pos).Cross(axis_dir); + if (check.Dot(check) < LENGTH_EPS) { + //! @todo isn't this the same as &(ent[i])? + Entity *ep = SK.GetEntity(e->h); + Entity en = {}; + // A point gets extruded to form a line segment + en.point[0] = Remap(ep->h, REMAP_LATHE_START); + en.point[1] = Remap(ep->h, REMAP_LATHE_END); + en.group = h; + en.construction = ep->construction; + en.style = ep->style; + en.h = Remap(ep->h, REMAP_PT_TO_LINE); + en.type = Entity::Type::LINE_SEGMENT; + entity->Add(&en); + } + } + } + MakeRevolveEndFaces(entity, pt, ai, af); + return; + } + + case Type::TRANSLATE: { + // inherit meshCombine from source group + Group *srcg = SK.GetGroup(opA); + meshCombine = srcg->meshCombine; + // The translation vector + AddParam(param, h.param(0), gp.x); + AddParam(param, h.param(1), gp.y); + AddParam(param, h.param(2), gp.z); + + int n = (int)valA, a0 = 0; + if(subtype == Subtype::ONE_SIDED && skipFirst) { + a0++; n++; + } + + for(a = a0; a < n; a++) { + // Not using range-for here because we're changing the size of entity in the loop. + for(i = 0; i < entity->n; i++) { + Entity *e = &(entity->Get(i)); + if(e->group != opA) continue; + + e->CalculateNumerical(/*forExport=*/false); + CopyEntity(entity, e, + a*2 - (subtype == Subtype::ONE_SIDED ? 0 : (n-1)), + (a == (n - 1)) ? REMAP_LAST : a, + h.param(0), h.param(1), h.param(2), + NO_PARAM, NO_PARAM, NO_PARAM, NO_PARAM, NO_PARAM, + CopyAs::N_TRANS); + } + } + return; + } + case Type::ROTATE: { + // inherit meshCombine from source group + Group *srcg = SK.GetGroup(opA); + meshCombine = srcg->meshCombine; + // The center of rotation + AddParam(param, h.param(0), gc.x); + AddParam(param, h.param(1), gc.y); + AddParam(param, h.param(2), gc.z); + // The rotation quaternion + AddParam(param, h.param(3), 30*PI/180); + AddParam(param, h.param(4), gn.x); + AddParam(param, h.param(5), gn.y); + AddParam(param, h.param(6), gn.z); + + int n = (int)valA, a0 = 0; + if(subtype == Subtype::ONE_SIDED && skipFirst) { + a0++; n++; + } + + for(a = a0; a < n; a++) { + // Not using range-for here because we're changing the size of entity in the loop. + for(i = 0; i < entity->n; i++) { + Entity *e = &(entity->Get(i)); + if(e->group != opA) continue; + + e->CalculateNumerical(/*forExport=*/false); + CopyEntity(entity, e, + a*2 - (subtype == Subtype::ONE_SIDED ? 0 : (n-1)), + (a == (n - 1)) ? REMAP_LAST : a, + h.param(0), h.param(1), h.param(2), + h.param(3), h.param(4), h.param(5), h.param(6), NO_PARAM, + CopyAs::N_ROT_AA); + } + } + return; + } + case Type::LINKED: + // The translation vector + AddParam(param, h.param(0), gp.x); + AddParam(param, h.param(1), gp.y); + AddParam(param, h.param(2), gp.z); + // The rotation quaternion + AddParam(param, h.param(3), 1); + AddParam(param, h.param(4), 0); + AddParam(param, h.param(5), 0); + AddParam(param, h.param(6), 0); + + // Not using range-for here because we're changing the size of entity in the loop. + for(i = 0; i < impEntity.n; i++) { + Entity *ie = &(impEntity[i]); + CopyEntity(entity, ie, 0, 0, + h.param(0), h.param(1), h.param(2), + h.param(3), h.param(4), h.param(5), h.param(6), NO_PARAM, + CopyAs::N_ROT_TRANS); + } + return; + } + ssassert(false, "Unexpected group type"); +} + +bool Group::IsSolvedOkay() { + return this->solved.how == SolveResult::OKAY || + (this->allowRedundant && this->solved.how == SolveResult::REDUNDANT_OKAY); +} + +void Group::AddEq(IdList *l, Expr *expr, int index) { + Equation eq; + eq.e = expr; + eq.h = h.equation(index); + l->Add(&eq); +} + +void Group::GenerateEquations(IdList *l) { + if(type == Type::LINKED) { + // Normalize the quaternion + ExprQuaternion q = { + Expr::From(h.param(3)), + Expr::From(h.param(4)), + Expr::From(h.param(5)), + Expr::From(h.param(6)) }; + AddEq(l, (q.Magnitude())->Minus(Expr::From(1)), 0); + } else if(type == Type::ROTATE || type == Type::REVOLVE || type == Type::HELIX) { + // The axis and center of rotation are specified numerically +#define EC(x) (Expr::From(x)) +#define EP(x) (Expr::From(h.param(x))) + ExprVector orig = SK.GetEntity(predef.origin)->PointGetExprs(); + AddEq(l, (orig.x)->Minus(EP(0)), 0); + AddEq(l, (orig.y)->Minus(EP(1)), 1); + AddEq(l, (orig.z)->Minus(EP(2)), 2); + // param 3 is the angle, which is free + Vector axis = SK.GetEntity(predef.entityB)->VectorGetNum(); + axis = axis.WithMagnitude(1); + AddEq(l, (EC(axis.x))->Minus(EP(4)), 3); + AddEq(l, (EC(axis.y))->Minus(EP(5)), 4); + AddEq(l, (EC(axis.z))->Minus(EP(6)), 5); +#undef EC +#undef EP + if(type == Type::HELIX) { + if(valB != 0.0) { + AddEq(l, Expr::From(h.param(7))->Times(Expr::From(PI))-> + Minus(Expr::From(h.param(3))->Times(Expr::From(valB))), 6); + } + } + } else if((type == Type::EXTRUDE) && (subtype != Subtype::ONE_SKEWED) && + (subtype != Subtype::TWO_SKEWED)) { + if(predef.entityB != Entity::FREE_IN_3D) { + // The extrusion path is locked along a line, normal to the + // specified workplane. Don't constrain for skewed extrusions. + Entity *w = SK.GetEntity(predef.entityB); + ExprVector u = w->Normal()->NormalExprsU(); + ExprVector v = w->Normal()->NormalExprsV(); + ExprVector extruden = { + Expr::From(h.param(0)), + Expr::From(h.param(1)), + Expr::From(h.param(2)) }; + + AddEq(l, u.Dot(extruden), 0); + AddEq(l, v.Dot(extruden), 1); + } + } else if(type == Type::TRANSLATE) { + if(predef.entityB != Entity::FREE_IN_3D) { + Entity *w = SK.GetEntity(predef.entityB); + ExprVector n = w->Normal()->NormalExprsN(); + ExprVector trans; + trans = ExprVector::From(h.param(0), h.param(1), h.param(2)); + + // The translation vector is parallel to the workplane + AddEq(l, trans.Dot(n), 0); + } + } +} + +hEntity Group::Remap(hEntity in, int copyNumber) { + auto it = remap.find({ in, copyNumber }); + if(it == remap.end()) { + // Due to the way the remap value is combined with the group handle into a 32-bit + // handle value to generate an entity handle, the mapped value must fit in a 16-bit + // variable. + // This limit can be lifted once the handle values are extended to 64-bit. + ssassert(remap.size() < (1 << 16) - 1, "Too many entities in group"); + std::tie(it, std::ignore) = + remap.insert({ { in, copyNumber }, { (uint32_t)remap.size() + 1 } }); + } + return h.entity(it->second.v); +} + +void Group::MakeExtrusionLines(EntityList *el, hEntity in) { + Entity *ep = SK.GetEntity(in); + + Entity en = {}; + if(ep->IsPoint()) { + // A point gets extruded to form a line segment + en.point[0] = Remap(ep->h, REMAP_TOP); + en.point[1] = Remap(ep->h, REMAP_BOTTOM); + en.group = h; + en.construction = ep->construction; + en.style = ep->style; + en.h = Remap(ep->h, REMAP_PT_TO_LINE); + en.type = Entity::Type::LINE_SEGMENT; + el->Add(&en); + } else if(ep->type == Entity::Type::LINE_SEGMENT) { + // A line gets extruded to form a plane face; an endpoint of the + // original line is a point in the plane, and the line is in the plane. + Vector a = SK.GetEntity(ep->point[0])->PointGetNum(); + Vector b = SK.GetEntity(ep->point[1])->PointGetNum(); + Vector ab = b.Minus(a); + + en.param[0] = h.param(0); + en.param[1] = h.param(1); + en.param[2] = h.param(2); + en.numPoint = a; + // we need a point somewhere on the face, and it must be from the current group. + en.point[0] = Remap(ep->point[0], REMAP_TOP); + en.numNormal = Quaternion::From(0, ab.x, ab.y, ab.z); + + en.group = h; + en.construction = ep->construction; + en.style = ep->style; + en.h = Remap(ep->h, REMAP_LINE_TO_FACE); + en.type = Entity::Type::FACE_XPROD; + el->Add(&en); + } +} + +void Group::MakeLatheCircles(EntityList *el, ParamList *param, hEntity in, Vector pt, Vector axis) { + Entity *ep = SK.GetEntity(in); + + Entity en = {}; + if(ep->IsPoint()) { + // A point gets revolved to form an arc. + en.point[0] = Remap(ep->h, REMAP_LATHE_ARC_CENTER); + en.point[1] = Remap(ep->h, REMAP_LATHE_START); + en.point[2] = en.point[1]; //Remap(ep->h, REMAP_LATHE_END); + + // Get arc center and point on arc. + Entity *pc = SK.GetEntity(en.point[0]); + Entity *pp = SK.GetEntity(en.point[1]); + + // Project arc point to the revolution axis and use it for arc center. + double k = pp->numPoint.Minus(pt).Dot(axis) / axis.Dot(axis); + pc->numPoint = pt.Plus(axis.ScaledBy(k)); + + // Create arc entity. + en.group = h; + en.construction = ep->construction; + en.style = ep->style; + en.h = Remap(ep->h, REMAP_PT_TO_ARC); + en.type = Entity::Type::ARC_OF_CIRCLE; + + // Generate a normal. + Entity n = {}; + n.workplane = en.workplane; + n.h = Remap(ep->h, REMAP_PT_TO_NORMAL); + n.group = en.group; + n.style = en.style; + n.type = Entity::Type::NORMAL_N_COPY; + + // Create basis for the normal. + Vector nu = pp->numPoint.Minus(pc->numPoint).WithMagnitude(1.0); + Vector nv = nu.Cross(axis).WithMagnitude(1.0); + n.numNormal = Quaternion::From(nv, nu); + + // The point determines where the normal gets displayed on-screen; + // it's entirely cosmetic. + n.point[0] = en.point[0]; + el->Add(&n); + en.normal = n.h; + el->Add(&en); + } +} + +void Group::MakeLatheSurfacesSelectable(EntityList *el, hEntity in, Vector axis) { + Entity *ep = SK.GetEntity(in); + + Entity en = {}; + if(ep->type == Entity::Type::LINE_SEGMENT) { + // An axis-perpendicular line gets revolved to form a face. + Vector a = SK.GetEntity(ep->point[0])->PointGetNum(); + Vector b = SK.GetEntity(ep->point[1])->PointGetNum(); + Vector u = b.Minus(a).WithMagnitude(1.0); + + // Check for perpendicularity: calculate cosine of the angle + // between axis and line direction and check that + // cos(angle) == 0 <-> angle == +-90 deg. + if(fabs(u.Dot(axis) / axis.Magnitude()) < ANGLE_COS_EPS) { + en.param[0] = h.param(0); + en.param[1] = h.param(1); + en.param[2] = h.param(2); + Vector v = axis.Cross(u).WithMagnitude(1.0); + Vector n = u.Cross(v); + en.numNormal = Quaternion::From(0, n.x, n.y, n.z); + + en.group = h; + en.construction = ep->construction; + en.style = ep->style; + en.h = Remap(ep->h, REMAP_LINE_TO_FACE); + en.type = Entity::Type::FACE_NORMAL_PT; + en.point[0] = Remap(ep->point[0], REMAP_LATHE_START); + el->Add(&en); + } + } +} + +// For Revolve and Helix groups the end faces are remapped from an arbitrary +// point on the sketch. We reference the transformed point but there is +// no existing normal so we need to define the rotation and timesApplied. +void Group::MakeRevolveEndFaces(EntityList *el, hEntity pt, int ai, int af) +{ + if(pt.v == 0) return; + Group *src = SK.GetGroup(opA); + Vector n = src->polyLoops.normal; + + // When there is no loop normal (e.g. if the loop is broken), use normal of workplane + // as fallback, to avoid breaking constraints depending on the faces. + if(n.Equals(Vector::From(0.0, 0.0, 0.0)) && src->type == Group::Type::DRAWING_WORKPLANE) { + n = SK.GetEntity(src->h.entity(0))->Normal()->NormalN(); + } + + Entity en = {}; + en.type = Entity::Type::FACE_ROT_NORMAL_PT; + en.group = h; + // The center of rotation + en.param[0] = h.param(0); + en.param[1] = h.param(1); + en.param[2] = h.param(2); + // The rotation quaternion + en.param[3] = h.param(3); + en.param[4] = h.param(4); + en.param[5] = h.param(5); + en.param[6] = h.param(6); + + en.numNormal = Quaternion::From(0, n.x, n.y, n.z); + en.point[0] = Remap(pt, REMAP_LATHE_START); + en.timesApplied = ai; + en.h = Remap(Entity::NO_ENTITY, REMAP_LATHE_START); + el->Add(&en); + + en.point[0] = Remap(pt, REMAP_LATHE_END); + en.timesApplied = af; + en.h = Remap(Entity::NO_ENTITY, REMAP_LATHE_END); + el->Add(&en); +} + +void Group::MakeExtrusionTopBottomFaces(EntityList *el, hEntity pt) +{ + if(pt.v == 0) return; + Group *src = SK.GetGroup(opA); + Vector n = src->polyLoops.normal; + + // When there is no loop normal (e.g. if the loop is broken), use normal of workplane + // as fallback, to avoid breaking constraints depending on the faces. + if(n.Equals(Vector::From(0.0, 0.0, 0.0)) && src->type == Group::Type::DRAWING_WORKPLANE) { + n = SK.GetEntity(src->h.entity(0))->Normal()->NormalN(); + } + + Entity en = {}; + en.type = Entity::Type::FACE_NORMAL_PT; + en.group = h; + + en.numNormal = Quaternion::From(0, n.x, n.y, n.z); + en.point[0] = Remap(pt, REMAP_TOP); + en.h = Remap(Entity::NO_ENTITY, REMAP_TOP); + el->Add(&en); + + en.point[0] = Remap(pt, REMAP_BOTTOM); + en.h = Remap(Entity::NO_ENTITY, REMAP_BOTTOM); + el->Add(&en); +} + +void Group::CopyEntity(EntityList *el, + Entity *ep, int timesApplied, int remap, + hParam dx, hParam dy, hParam dz, + hParam qw, hParam qvx, hParam qvy, hParam qvz, hParam dist, + CopyAs as) +{ + Entity en = {}; + en.type = ep->type; + en.extraPoints = ep->extraPoints; + en.h = Remap(ep->h, remap); + en.timesApplied = timesApplied; + en.group = h; + en.construction = ep->construction; + en.style = ep->style; + en.str = ep->str; + en.font = ep->font; + en.file = ep->file; + + switch(ep->type) { + case Entity::Type::WORKPLANE: + // Don't copy these. + return; + + case Entity::Type::POINT_N_COPY: + case Entity::Type::POINT_N_TRANS: + case Entity::Type::POINT_N_ROT_TRANS: + case Entity::Type::POINT_N_ROT_AA: + case Entity::Type::POINT_N_ROT_AXIS_TRANS: + case Entity::Type::POINT_IN_3D: + case Entity::Type::POINT_IN_2D: + if(as == CopyAs::N_TRANS) { + en.type = Entity::Type::POINT_N_TRANS; + en.param[0] = dx; + en.param[1] = dy; + en.param[2] = dz; + } else if(as == CopyAs::NUMERIC) { + en.type = Entity::Type::POINT_N_COPY; + } else { + if(as == CopyAs::N_ROT_AA) { + en.type = Entity::Type::POINT_N_ROT_AA; + } else if (as == CopyAs::N_ROT_AXIS_TRANS) { + en.type = Entity::Type::POINT_N_ROT_AXIS_TRANS; + } else { + en.type = Entity::Type::POINT_N_ROT_TRANS; + } + en.param[0] = dx; + en.param[1] = dy; + en.param[2] = dz; + en.param[3] = qw; + en.param[4] = qvx; + en.param[5] = qvy; + en.param[6] = qvz; + if (as == CopyAs::N_ROT_AXIS_TRANS) { + en.param[7] = dist; + } + } + en.numPoint = (ep->actPoint).ScaledBy(scale); + break; + + case Entity::Type::NORMAL_N_COPY: + case Entity::Type::NORMAL_N_ROT: + case Entity::Type::NORMAL_N_ROT_AA: + case Entity::Type::NORMAL_IN_3D: + case Entity::Type::NORMAL_IN_2D: + if(as == CopyAs::N_TRANS || as == CopyAs::NUMERIC) { + en.type = Entity::Type::NORMAL_N_COPY; + } else { // N_ROT_AXIS_TRANS probably doesn't warrant a new entity Type + if(as == CopyAs::N_ROT_AA || as == CopyAs::N_ROT_AXIS_TRANS) { + en.type = Entity::Type::NORMAL_N_ROT_AA; + } else { + en.type = Entity::Type::NORMAL_N_ROT; + } + en.param[0] = qw; + en.param[1] = qvx; + en.param[2] = qvy; + en.param[3] = qvz; + } + en.numNormal = ep->actNormal; + if(scale < 0) en.numNormal = en.numNormal.Mirror(); + + en.point[0] = Remap(ep->point[0], remap); + break; + + case Entity::Type::DISTANCE_N_COPY: + case Entity::Type::DISTANCE: + en.type = Entity::Type::DISTANCE_N_COPY; + en.numDistance = ep->actDistance*fabs(scale); + break; + + case Entity::Type::FACE_NORMAL_PT: + case Entity::Type::FACE_XPROD: + case Entity::Type::FACE_N_ROT_TRANS: + case Entity::Type::FACE_N_TRANS: + case Entity::Type::FACE_N_ROT_AA: + case Entity::Type::FACE_ROT_NORMAL_PT: + case Entity::Type::FACE_N_ROT_AXIS_TRANS: + if(as == CopyAs::N_TRANS) { + en.type = Entity::Type::FACE_N_TRANS; + en.param[0] = dx; + en.param[1] = dy; + en.param[2] = dz; + } else if (as == CopyAs::NUMERIC) { + en.type = Entity::Type::FACE_NORMAL_PT; + } else if (as == CopyAs::N_ROT_AXIS_TRANS) { + en.type = Entity::Type::FACE_N_ROT_AXIS_TRANS; + en.param[0] = dx; + en.param[1] = dy; + en.param[2] = dz; + en.param[3] = qw; + en.param[4] = qvx; + en.param[5] = qvy; + en.param[6] = qvz; + en.param[7] = dist; + } else { + if(as == CopyAs::N_ROT_AA) { + en.type = Entity::Type::FACE_N_ROT_AA; + } else { + en.type = Entity::Type::FACE_N_ROT_TRANS; + } + en.param[0] = dx; + en.param[1] = dy; + en.param[2] = dz; + en.param[3] = qw; + en.param[4] = qvx; + en.param[5] = qvy; + en.param[6] = qvz; + } + en.numPoint = (ep->actPoint).ScaledBy(scale); + en.numNormal = (ep->actNormal).ScaledBy(scale); + // the new face needs an associated point (old files may not have one) + if(ep->point[0].v != 0) { + en.point[0] = Remap(ep->point[0], remap); + } + break; + + default: { + if((Entity::Type::IMAGE == ep->type) && (true == ep->construction)) { + // Do not copy image entities if they are construction. + return; + } + + int i, points; + bool hasNormal, hasDistance; + ssassert(EntReqTable::GetEntityInfo(ep->type, ep->extraPoints, + NULL, &points, &hasNormal, &hasDistance), "No entity info"); + for(i = 0; i < points; i++) { + en.point[i] = Remap(ep->point[i], remap); + } + if(hasNormal) en.normal = Remap(ep->normal, remap); + if(hasDistance) en.distance = Remap(ep->distance, remap); + break; + } + } + + // If the entity came from an linked file where it was invisible then + // ep->actiVisble will be false, and we should hide it. Or if the entity + // came from a copy (e.g. step and repeat) of a force-hidden linked + // entity, then we also want to hide it. + en.forceHidden = (!ep->actVisible) || ep->forceHidden; + + el->Add(&en); +} + +bool Group::ShouldDrawExploded() const { + return SS.explode && h == SS.GW.activeGroup && type == Type::DRAWING_WORKPLANE && !SS.exportMode; +} + +} // namespace SolveSpace diff --git a/src/groupmesh.cpp b/src/groupmesh.cpp new file mode 100644 index 0000000..785817b --- /dev/null +++ b/src/groupmesh.cpp @@ -0,0 +1,803 @@ +//----------------------------------------------------------------------------- +// Routines to generate our watertight brep shells from the operations +// and entities specified by the user in each group; templated to work either +// on an SShell of ratpoly surfaces or on an SMesh of triangles. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +void Group::AssembleLoops(bool *allClosed, + bool *allCoplanar, + bool *allNonZeroLen) +{ + SBezierList sbl = {}; + + int i; + for(auto &e : SK.entity) { + if(e.group != h) + continue; + if(e.construction) + continue; + if(e.forceHidden) + continue; + + e.GenerateBezierCurves(&sbl); + } + + SBezier *sb; + *allNonZeroLen = true; + for(sb = sbl.l.First(); sb; sb = sbl.l.NextAfter(sb)) { + for(i = 1; i <= sb->deg; i++) { + if(!(sb->ctrl[i]).Equals(sb->ctrl[0])) { + break; + } + } + if(i > sb->deg) { + // This is a zero-length edge. + *allNonZeroLen = false; + polyError.errorPointAt = sb->ctrl[0]; + goto done; + } + } + + // Try to assemble all these Beziers into loops. The closed loops go into + // bezierLoops, with the outer loops grouped with their holes. The + // leftovers, if any, go in bezierOpens. + bezierLoops.FindOuterFacesFrom(&sbl, &polyLoops, NULL, + SS.ChordTolMm(), + allClosed, &(polyError.notClosedAt), + allCoplanar, &(polyError.errorPointAt), + &bezierOpens); + done: + sbl.Clear(); +} + +void Group::GenerateLoops() { + polyLoops.Clear(); + bezierLoops.Clear(); + bezierOpens.Clear(); + + if(type == Type::DRAWING_3D || type == Type::DRAWING_WORKPLANE || + type == Type::ROTATE || type == Type::TRANSLATE || type == Type::LINKED) + { + bool allClosed = false, allCoplanar = false, allNonZeroLen = false; + AssembleLoops(&allClosed, &allCoplanar, &allNonZeroLen); + if(!allNonZeroLen) { + polyError.how = PolyError::ZERO_LEN_EDGE; + } else if(!allCoplanar) { + polyError.how = PolyError::NOT_COPLANAR; + } else if(!allClosed) { + polyError.how = PolyError::NOT_CLOSED; + } else { + polyError.how = PolyError::GOOD; + // The self-intersecting check is kind of slow, so don't run it + // unless requested. + if(SS.checkClosedContour) { + if(polyLoops.SelfIntersecting(&(polyError.errorPointAt))) { + polyError.how = PolyError::SELF_INTERSECTING; + } + } + } + } +} + +void SShell::RemapFaces(Group *g, int remap) { + for(SSurface &ss : surface){ + hEntity face = { ss.face }; + if(face == Entity::NO_ENTITY) continue; + + face = g->Remap(face, remap); + ss.face = face.v; + } +} + +void SMesh::RemapFaces(Group *g, int remap) { + STriangle *tr; + for(tr = l.First(); tr; tr = l.NextAfter(tr)) { + hEntity face = { tr->meta.face }; + if(face == Entity::NO_ENTITY) continue; + + face = g->Remap(face, remap); + tr->meta.face = face.v; + } +} + +template +void Group::GenerateForStepAndRepeat(T *steps, T *outs, Group::CombineAs forWhat) { + + int n = (int)valA, a0 = 0; + if(subtype == Subtype::ONE_SIDED && skipFirst) { + a0++; n++; + } + + int a; + // create all the transformed copies + std::vector transd(n); + std::vector workA(n); + workA[0] = {}; + // first generate a shell/mesh with each transformed copy +#pragma omp parallel for + for(a = a0; a < n; a++) { + transd[a] = {}; + workA[a] = {}; + int ap = a*2 - (subtype == Subtype::ONE_SIDED ? 0 : (n-1)); + + if(type == Type::TRANSLATE) { + Vector trans = Vector::From(h.param(0), h.param(1), h.param(2)); + trans = trans.ScaledBy(ap); + transd[a].MakeFromTransformationOf(steps, + trans, Quaternion::IDENTITY, 1.0); + } else { + Vector trans = Vector::From(h.param(0), h.param(1), h.param(2)); + double theta = ap * SK.GetParam(h.param(3))->val; + double c = cos(theta), s = sin(theta); + Vector axis = Vector::From(h.param(4), h.param(5), h.param(6)); + Quaternion q = Quaternion::From(c, s*axis.x, s*axis.y, s*axis.z); + // Rotation is centered at t; so A(x - t) + t = Ax + (t - At) + transd[a].MakeFromTransformationOf(steps, + trans.Minus(q.Rotate(trans)), q, 1.0); + } + } + for(a = a0; a < n; a++) { + // We need to rewrite any plane face entities to the transformed ones. + int remap = (a == (n - 1)) ? REMAP_LAST : a; + transd[a].RemapFaces(this, remap); + } + + std::vector *soFar = &transd; + std::vector *scratch = &workA; + // do the boolean operations on pairs of equal size + while(n > 1) { + for(a = 0; a < n; a+=2) { + scratch->at(a/2).Clear(); + // combine a pair of shells + if((a==0) && (a0==1)) { // if the first was skipped just copy the 2nd + scratch->at(a/2).MakeFromCopyOf(&(soFar->at(a+1))); + (soFar->at(a+1)).Clear(); + a0 = 0; + } else if (a == n-1) { // for an odd number just copy the last one + scratch->at(a/2).MakeFromCopyOf(&(soFar->at(a))); + (soFar->at(a)).Clear(); + } else if(forWhat == CombineAs::ASSEMBLE) { + scratch->at(a/2).MakeFromAssemblyOf(&(soFar->at(a)), &(soFar->at(a+1))); + (soFar->at(a)).Clear(); + (soFar->at(a+1)).Clear(); + } else { + scratch->at(a/2).MakeFromUnionOf(&(soFar->at(a)), &(soFar->at(a+1))); + (soFar->at(a)).Clear(); + (soFar->at(a+1)).Clear(); + } + } + swap(scratch, soFar); + n = (n+1)/2; + } + outs->Clear(); + *outs = soFar->at(0); +} + +template +void Group::GenerateForBoolean(T *prevs, T *thiss, T *outs, Group::CombineAs how) { + // If this group contributes no new mesh, then our running mesh is the + // same as last time, no combining required. Likewise if we have a mesh + // but it's suppressed. + if(thiss->IsEmpty() || suppress) { + outs->MakeFromCopyOf(prevs); + return; + } + + // So our group's shell appears in thisShell. Combine this with the + // previous group's shell, using the requested operation. + switch(how) { + case CombineAs::UNION: + outs->MakeFromUnionOf(prevs, thiss); + break; + + case CombineAs::DIFFERENCE: + outs->MakeFromDifferenceOf(prevs, thiss); + break; + + case CombineAs::INTERSECTION: + outs->MakeFromIntersectionOf(prevs, thiss); + break; + + case CombineAs::ASSEMBLE: + outs->MakeFromAssemblyOf(prevs, thiss); + break; + } +} + +void Group::GenerateShellAndMesh() { + bool prevBooleanFailed = booleanFailed; + booleanFailed = false; + + Group *srcg = this; + + thisShell.Clear(); + thisMesh.Clear(); + runningShell.Clear(); + runningMesh.Clear(); + + // Don't attempt a lathe or extrusion unless the source section is good: + // planar and not self-intersecting. + bool haveSrc = true; + if(type == Type::EXTRUDE || type == Type::LATHE || type == Type::REVOLVE) { + Group *src = SK.GetGroup(opA); + if(src->polyError.how != PolyError::GOOD) { + haveSrc = false; + } + } + + if(type == Type::TRANSLATE || type == Type::ROTATE) { + // A step and repeat gets merged against the group's previous group, + // not our own previous group. + srcg = SK.GetGroup(opA); + + if(!srcg->suppress) { + if(!IsForcedToMesh()) { + GenerateForStepAndRepeat(&(srcg->thisShell), &thisShell, srcg->meshCombine); + } else { + SMesh prevm = {}; + prevm.MakeFromCopyOf(&srcg->thisMesh); + srcg->thisShell.TriangulateInto(&prevm); + GenerateForStepAndRepeat (&prevm, &thisMesh, srcg->meshCombine); + } + } + } else if(type == Type::EXTRUDE && haveSrc) { + Group *src = SK.GetGroup(opA); + Vector translate = Vector::From(h.param(0), h.param(1), h.param(2)); + + Vector tbot, ttop; + if(subtype == Subtype::ONE_SIDED || subtype == Subtype::ONE_SKEWED) { + tbot = Vector::From(0, 0, 0); ttop = translate.ScaledBy(2); + } else { + tbot = translate.ScaledBy(-1); ttop = translate.ScaledBy(1); + } + + SBezierLoopSetSet *sblss = &(src->bezierLoops); + SBezierLoopSet *sbls; + for(sbls = sblss->l.First(); sbls; sbls = sblss->l.NextAfter(sbls)) { + int is = thisShell.surface.n; + // Extrude this outer contour (plus its inner contours, if present) + thisShell.MakeFromExtrusionOf(sbls, tbot, ttop, color); + + // And for any plane faces, annotate the model with the entity for + // that face, so that the user can select them with the mouse. + Vector onOrig = sbls->point; + int i; + // Not using range-for here because we're starting at a different place and using + // indices for meaning. + for(i = is; i < thisShell.surface.n; i++) { + SSurface *ss = &(thisShell.surface[i]); + hEntity face = Entity::NO_ENTITY; + + Vector p = ss->PointAt(0, 0), + n = ss->NormalAt(0, 0).WithMagnitude(1); + double d = n.Dot(p); + + if(i == is || i == (is + 1)) { + // These are the top and bottom of the shell. + if(fabs((onOrig.Plus(ttop)).Dot(n) - d) < LENGTH_EPS) { + face = Remap(Entity::NO_ENTITY, REMAP_TOP); + ss->face = face.v; + } + if(fabs((onOrig.Plus(tbot)).Dot(n) - d) < LENGTH_EPS) { + face = Remap(Entity::NO_ENTITY, REMAP_BOTTOM); + ss->face = face.v; + } + continue; + } + + // So these are the sides + if(ss->degm != 1 || ss->degn != 1) continue; + + for(Entity &e : SK.entity) { + if(e.group != opA) continue; + if(e.type != Entity::Type::LINE_SEGMENT) continue; + + Vector a = SK.GetEntity(e.point[0])->PointGetNum(), + b = SK.GetEntity(e.point[1])->PointGetNum(); + a = a.Plus(ttop); + b = b.Plus(ttop); + // Could get taken backwards, so check all cases. + if((a.Equals(ss->ctrl[0][0]) && b.Equals(ss->ctrl[1][0])) || + (b.Equals(ss->ctrl[0][0]) && a.Equals(ss->ctrl[1][0])) || + (a.Equals(ss->ctrl[0][1]) && b.Equals(ss->ctrl[1][1])) || + (b.Equals(ss->ctrl[0][1]) && a.Equals(ss->ctrl[1][1]))) + { + face = Remap(e.h, REMAP_LINE_TO_FACE); + ss->face = face.v; + break; + } + } + } + } + } else if(type == Type::LATHE && haveSrc) { + Group *src = SK.GetGroup(opA); + + Vector pt = SK.GetEntity(predef.origin)->PointGetNum(), + axis = SK.GetEntity(predef.entityB)->VectorGetNum(); + axis = axis.WithMagnitude(1); + + SBezierLoopSetSet *sblss = &(src->bezierLoops); + SBezierLoopSet *sbls; + for(sbls = sblss->l.First(); sbls; sbls = sblss->l.NextAfter(sbls)) { + thisShell.MakeFromRevolutionOf(sbls, pt, axis, color, this); + } + } else if(type == Type::REVOLVE && haveSrc) { + Group *src = SK.GetGroup(opA); + double anglef = SK.GetParam(h.param(3))->val * 4; // why the 4 is needed? + double dists = 0, distf = 0; + double angles = 0.0; + if(subtype != Subtype::ONE_SIDED) { + anglef *= 0.5; + angles = -anglef; + } + Vector pt = SK.GetEntity(predef.origin)->PointGetNum(), + axis = SK.GetEntity(predef.entityB)->VectorGetNum(); + axis = axis.WithMagnitude(1); + + SBezierLoopSetSet *sblss = &(src->bezierLoops); + SBezierLoopSet *sbls; + for(sbls = sblss->l.First(); sbls; sbls = sblss->l.NextAfter(sbls)) { + if(fabs(anglef - angles) < 2 * PI) { + thisShell.MakeFromHelicalRevolutionOf(sbls, pt, axis, color, this, + angles, anglef, dists, distf); + } else { + thisShell.MakeFromRevolutionOf(sbls, pt, axis, color, this); + } + } + } else if(type == Type::HELIX && haveSrc) { + Group *src = SK.GetGroup(opA); + double anglef = SK.GetParam(h.param(3))->val * 4; // why the 4 is needed? + double dists = 0, distf = 0; + double angles = 0.0; + distf = SK.GetParam(h.param(7))->val * 2; // dist is applied twice + if(subtype != Subtype::ONE_SIDED) { + anglef *= 0.5; + angles = -anglef; + distf *= 0.5; + dists = -distf; + } + Vector pt = SK.GetEntity(predef.origin)->PointGetNum(), + axis = SK.GetEntity(predef.entityB)->VectorGetNum(); + axis = axis.WithMagnitude(1); + + SBezierLoopSetSet *sblss = &(src->bezierLoops); + SBezierLoopSet *sbls; + for(sbls = sblss->l.First(); sbls; sbls = sblss->l.NextAfter(sbls)) { + thisShell.MakeFromHelicalRevolutionOf(sbls, pt, axis, color, this, + angles, anglef, dists, distf); + } + } else if(type == Type::LINKED) { + // The imported shell or mesh are copied over, with the appropriate + // transformation applied. We also must remap the face entities. + Vector offset = { + SK.GetParam(h.param(0))->val, + SK.GetParam(h.param(1))->val, + SK.GetParam(h.param(2))->val }; + Quaternion q = { + SK.GetParam(h.param(3))->val, + SK.GetParam(h.param(4))->val, + SK.GetParam(h.param(5))->val, + SK.GetParam(h.param(6))->val }; + + thisMesh.MakeFromTransformationOf(&impMesh, offset, q, scale); + thisMesh.RemapFaces(this, 0); + + thisShell.MakeFromTransformationOf(&impShell, offset, q, scale); + thisShell.RemapFaces(this, 0); + } + + if(srcg->meshCombine != CombineAs::ASSEMBLE) { + thisShell.MergeCoincidentSurfaces(); + } + + // So now we've got the mesh or shell for this group. Combine it with + // the previous group's mesh or shell with the requested Boolean, and + // we're done. + + Group *prevg = srcg->RunningMeshGroup(); + + if(!IsForcedToMesh()) { + SShell *prevs = &(prevg->runningShell); + GenerateForBoolean(prevs, &thisShell, &runningShell, + srcg->meshCombine); + + if(srcg->meshCombine != CombineAs::ASSEMBLE) { + runningShell.MergeCoincidentSurfaces(); + } + + // If the Boolean failed, then we should note that in the text screen + // for this group. + booleanFailed = runningShell.booleanFailed; + if(booleanFailed != prevBooleanFailed) { + SS.ScheduleShowTW(); + } + } else { + SMesh prevm, thism; + prevm = {}; + thism = {}; + + prevm.MakeFromCopyOf(&(prevg->runningMesh)); + prevg->runningShell.TriangulateInto(&prevm); + + thism.MakeFromCopyOf(&thisMesh); + thisShell.TriangulateInto(&thism); + + SMesh outm = {}; + GenerateForBoolean(&prevm, &thism, &outm, srcg->meshCombine); + + // Remove degenerate triangles; if we don't, they'll get split in SnapToMesh + // in every generated group, resulting in polynomial increase in triangle count, + // and corresponding slowdown. + outm.RemoveDegenerateTriangles(); + + if(srcg->meshCombine != CombineAs::ASSEMBLE) { + // And make sure that the output mesh is vertex-to-vertex. + SKdNode *root = SKdNode::From(&outm); + root->SnapToMesh(&outm); + root->MakeMeshInto(&runningMesh); + } else { + runningMesh.MakeFromCopyOf(&outm); + } + + outm.Clear(); + thism.Clear(); + prevm.Clear(); + } + + displayDirty = true; +} + +void Group::GenerateDisplayItems() { + // This is potentially slow (since we've got to triangulate a shell, or + // to find the emphasized edges for a mesh), so we will run it only + // if its inputs have changed. + if(displayDirty) { + Group *pg = RunningMeshGroup(); + if(pg && thisMesh.IsEmpty() && thisShell.IsEmpty()) { + // We don't contribute any new solid model in this group, so our + // display items are identical to the previous group's; which means + // that we can just display those, and stop ourselves from + // recalculating for those every time we get a change in this group. + // + // Note that this can end up recursing multiple times (if multiple + // groups that contribute no solid model exist in sequence), but + // that's okay. + pg->GenerateDisplayItems(); + + displayMesh.Clear(); + displayMesh.MakeFromCopyOf(&(pg->displayMesh)); + + displayOutlines.Clear(); + if(SS.GW.showEdges || SS.GW.showOutlines) { + displayOutlines.MakeFromCopyOf(&pg->displayOutlines); + } + } else { + // We do contribute new solid model, so we have to triangulate the + // shell, and edge-find the mesh. + displayMesh.Clear(); + runningShell.TriangulateInto(&displayMesh); + STriangle *t; + for(t = runningMesh.l.First(); t; t = runningMesh.l.NextAfter(t)) { + STriangle trn = *t; + Vector n = trn.Normal(); + trn.an = n; + trn.bn = n; + trn.cn = n; + displayMesh.AddTriangle(&trn); + } + + displayOutlines.Clear(); + + if(SS.GW.showEdges || SS.GW.showOutlines) { + SOutlineList rawOutlines = {}; + if(!runningMesh.l.IsEmpty()) { + // Triangle mesh only; no shell or emphasized edges. + runningMesh.MakeOutlinesInto(&rawOutlines, EdgeKind::EMPHASIZED); + } else { + displayMesh.MakeOutlinesInto(&rawOutlines, EdgeKind::SHARP); + } + + PolylineBuilder builder; + builder.MakeFromOutlines(rawOutlines); + builder.GenerateOutlines(&displayOutlines); + rawOutlines.Clear(); + } + } + + // If we render this mesh, we need to know whether it's transparent, + // and we'll want all transparent triangles last, to make the depth test + // work correctly. + displayMesh.PrecomputeTransparency(); + + // Recalculate mass center if needed + if(SS.centerOfMass.draw && SS.centerOfMass.dirty && h == SS.GW.activeGroup) { + SS.UpdateCenterOfMass(); + } + displayDirty = false; + } +} + +Group *Group::PreviousGroup() const { + Group *prev = nullptr; + for(auto const &gh : SK.groupOrder) { + Group *g = SK.GetGroup(gh); + if(g->h == h) { + return prev; + } + prev = g; + } + return nullptr; +} + +Group *Group::RunningMeshGroup() const { + if(type == Type::TRANSLATE || type == Type::ROTATE) { + return SK.GetGroup(opA)->RunningMeshGroup(); + } else { + return PreviousGroup(); + } +} + +bool Group::IsMeshGroup() { + switch(type) { + case Group::Type::EXTRUDE: + case Group::Type::LATHE: + case Group::Type::REVOLVE: + case Group::Type::HELIX: + case Group::Type::ROTATE: + case Group::Type::TRANSLATE: + return true; + + default: + return false; + } +} + +void Group::DrawMesh(DrawMeshAs how, Canvas *canvas) { + if(!(SS.GW.showShaded || + SS.GW.drawOccludedAs != GraphicsWindow::DrawOccludedAs::VISIBLE)) return; + + switch(how) { + case DrawMeshAs::DEFAULT: { + // Force the shade color to something dim to not distract from + // the sketch. + Canvas::Fill fillFront = {}; + if(!SS.GW.showShaded) { + fillFront.layer = Canvas::Layer::DEPTH_ONLY; + } + if((type == Type::DRAWING_3D || type == Type::DRAWING_WORKPLANE) + && SS.GW.dimSolidModel) { + fillFront.color = Style::Color(Style::DIM_SOLID); + } + Canvas::hFill hcfFront = canvas->GetFill(fillFront); + + // The back faces are drawn in red; should never seem them, since we + // draw closed shells, so that's a debugging aid. + Canvas::hFill hcfBack = {}; + if(SS.drawBackFaces && !displayMesh.isTransparent) { + Canvas::Fill fillBack = {}; + fillBack.layer = fillFront.layer; + fillBack.color = RgbaColor::FromFloat(1.0f, 0.1f, 0.1f); + hcfBack = canvas->GetFill(fillBack); + } else { + hcfBack = hcfFront; + } + + // Draw the shaded solid into the depth buffer for hidden line removal, + // and if we're actually going to display it, to the color buffer too. + canvas->DrawMesh(displayMesh, hcfFront, hcfBack); + + // Draw mesh edges, for debugging. + if(SS.GW.showMesh) { + Canvas::Stroke strokeTriangle = {}; + strokeTriangle.zIndex = 1; + strokeTriangle.color = RgbaColor::FromFloat(0.0f, 1.0f, 0.0f); + strokeTriangle.width = 1; + strokeTriangle.unit = Canvas::Unit::PX; + Canvas::hStroke hcsTriangle = canvas->GetStroke(strokeTriangle); + SEdgeList edges = {}; + for(const STriangle &t : displayMesh.l) { + edges.AddEdge(t.a, t.b); + edges.AddEdge(t.b, t.c); + edges.AddEdge(t.c, t.a); + } + canvas->DrawEdges(edges, hcsTriangle); + edges.Clear(); + } + break; + } + + case DrawMeshAs::HOVERED: { + Canvas::Fill fill = {}; + fill.color = Style::Color(Style::HOVERED); + fill.pattern = Canvas::FillPattern::CHECKERED_A; + fill.zIndex = 2; + Canvas::hFill hcf = canvas->GetFill(fill); + + std::vector faces; + hEntity he = SS.GW.hover.entity; + if(he.v != 0 && SK.GetEntity(he)->IsFace()) { + faces.push_back(he.v); + } + canvas->DrawFaces(displayMesh, faces, hcf); + break; + } + + case DrawMeshAs::SELECTED: { + Canvas::Fill fill = {}; + fill.color = Style::Color(Style::SELECTED); + fill.pattern = Canvas::FillPattern::CHECKERED_B; + fill.zIndex = 1; + Canvas::hFill hcf = canvas->GetFill(fill); + + std::vector faces; + SS.GW.GroupSelection(); + auto const &gs = SS.GW.gs; + // See also GraphicsWindow::MakeSelected "if(c >= MAX_SELECTABLE_FACES)" + // and GraphicsWindow::GroupSelection "if(e->IsFace())" + for(auto &fc : gs.face) { + faces.push_back(fc.v); + } + canvas->DrawFaces(displayMesh, faces, hcf); + break; + } + } +} + +void Group::Draw(Canvas *canvas) { + // Everything here gets drawn whether or not the group is hidden; we + // can control this stuff independently, with show/hide solids, edges, + // mesh, etc. + + GenerateDisplayItems(); + DrawMesh(DrawMeshAs::DEFAULT, canvas); + + if(SS.GW.showEdges) { + Canvas::Stroke strokeEdge = Style::Stroke(Style::SOLID_EDGE); + strokeEdge.zIndex = 1; + Canvas::hStroke hcsEdge = canvas->GetStroke(strokeEdge); + + canvas->DrawOutlines(displayOutlines, hcsEdge, + SS.GW.showOutlines + ? Canvas::DrawOutlinesAs::EMPHASIZED_WITHOUT_CONTOUR + : Canvas::DrawOutlinesAs::EMPHASIZED_AND_CONTOUR); + + if(SS.GW.drawOccludedAs != GraphicsWindow::DrawOccludedAs::INVISIBLE) { + Canvas::Stroke strokeHidden = Style::Stroke(Style::HIDDEN_EDGE); + if(SS.GW.drawOccludedAs == GraphicsWindow::DrawOccludedAs::VISIBLE) { + strokeHidden.stipplePattern = StipplePattern::CONTINUOUS; + } + strokeHidden.layer = Canvas::Layer::OCCLUDED; + Canvas::hStroke hcsHidden = canvas->GetStroke(strokeHidden); + + canvas->DrawOutlines(displayOutlines, hcsHidden, + Canvas::DrawOutlinesAs::EMPHASIZED_AND_CONTOUR); + } + } + + if(SS.GW.showOutlines) { + Canvas::Stroke strokeOutline = Style::Stroke(Style::OUTLINE); + strokeOutline.zIndex = 1; + Canvas::hStroke hcsOutline = canvas->GetStroke(strokeOutline); + + canvas->DrawOutlines(displayOutlines, hcsOutline, + Canvas::DrawOutlinesAs::CONTOUR_ONLY); + } +} + +void Group::DrawPolyError(Canvas *canvas) { + const Camera &camera = canvas->GetCamera(); + + Canvas::Stroke strokeUnclosed = Style::Stroke(Style::DRAW_ERROR); + strokeUnclosed.color = strokeUnclosed.color.WithAlpha(50); + Canvas::hStroke hcsUnclosed = canvas->GetStroke(strokeUnclosed); + + Canvas::Stroke strokeError = Style::Stroke(Style::DRAW_ERROR); + strokeError.layer = Canvas::Layer::FRONT; + strokeError.width = 1.0f; + Canvas::hStroke hcsError = canvas->GetStroke(strokeError); + + double textHeight = Style::DefaultTextHeight() / camera.scale; + + // And finally show the polygons too, and any errors if it's not possible + // to assemble the lines into closed polygons. + if(polyError.how == PolyError::NOT_CLOSED) { + // Report this error only in sketch-in-workplane groups; otherwise + // it's just a nuisance. + if(type == Type::DRAWING_WORKPLANE) { + canvas->DrawVectorText(_("not closed contour, or not all same style!"), + textHeight, + polyError.notClosedAt.b, camera.projRight, camera.projUp, + hcsError); + canvas->DrawLine(polyError.notClosedAt.a, polyError.notClosedAt.b, hcsUnclosed); + } + } else if(polyError.how == PolyError::NOT_COPLANAR || + polyError.how == PolyError::SELF_INTERSECTING || + polyError.how == PolyError::ZERO_LEN_EDGE) { + // These errors occur at points, not lines + if(type == Type::DRAWING_WORKPLANE) { + const char *msg; + if(polyError.how == PolyError::NOT_COPLANAR) { + msg = _("points not all coplanar!"); + } else if(polyError.how == PolyError::SELF_INTERSECTING) { + msg = _("contour is self-intersecting!"); + } else { + msg = _("zero-length edge!"); + } + canvas->DrawVectorText(msg, textHeight, + polyError.errorPointAt, camera.projRight, camera.projUp, + hcsError); + } + } else { + // The contours will get filled in DrawFilledPaths. + } +} + +void Group::DrawFilledPaths(Canvas *canvas) { + for(const SBezierLoopSet &sbls : bezierLoops.l) { + if(sbls.l.IsEmpty() || sbls.l[0].l.IsEmpty()) + continue; + + // In an assembled loop, all the styles should be the same; so doesn't + // matter which one we grab. + const SBezier *sb = &(sbls.l[0].l[0]); + Style *s = Style::Get({ (uint32_t)sb->auxA }); + + Canvas::Fill fill = {}; + fill.zIndex = 1; + if(s->filled) { + // This is a filled loop, where the user specified a fill color. + fill.color = s->fillColor; + } else if(h == SS.GW.activeGroup && SS.checkClosedContour && + polyError.how == PolyError::GOOD) { + // If this is the active group, and we are supposed to check + // for closed contours, and we do indeed have a closed and + // non-intersecting contour, then fill it dimly. + fill.color = Style::Color(Style::CONTOUR_FILL).WithAlpha(127); + } else continue; + Canvas::hFill hcf = canvas->GetFill(fill); + + SPolygon sp = {}; + sbls.MakePwlInto(&sp); + canvas->DrawPolygon(sp, hcf); + sp.Clear(); + } +} + +void Group::DrawContourAreaLabels(Canvas *canvas) { + const Camera &camera = canvas->GetCamera(); + Vector gr = camera.projRight.ScaledBy(1 / camera.scale); + Vector gu = camera.projUp.ScaledBy(1 / camera.scale); + + for(SBezierLoopSet &sbls : bezierLoops.l) { + if(sbls.l.IsEmpty() || sbls.l[0].l.IsEmpty()) + continue; + + Vector min = sbls.l[0].l[0].ctrl[0]; + Vector max = min; + Vector zero = Vector::From(0.0, 0.0, 0.0); + sbls.GetBoundingProjd(Vector::From(1.0, 0.0, 0.0), zero, &min.x, &max.x); + sbls.GetBoundingProjd(Vector::From(0.0, 1.0, 0.0), zero, &min.y, &max.y); + sbls.GetBoundingProjd(Vector::From(0.0, 0.0, 1.0), zero, &min.z, &max.z); + + Vector mid = min.Plus(max).ScaledBy(0.5); + + hStyle hs = { Style::CONSTRAINT }; + Canvas::Stroke stroke = Style::Stroke(hs); + stroke.layer = Canvas::Layer::FRONT; + + std::string label = SS.MmToStringSI(fabs(sbls.SignedArea()), /*dim=*/2); + double fontHeight = Style::TextHeight(hs); + double textWidth = VectorFont::Builtin()->GetWidth(fontHeight, label), + textHeight = VectorFont::Builtin()->GetCapHeight(fontHeight); + Vector pos = mid.Minus(gr.ScaledBy(textWidth / 2.0)) + .Minus(gu.ScaledBy(textHeight / 2.0)); + canvas->DrawVectorText(label, fontHeight, pos, gr, gu, canvas->GetStroke(stroke)); + } +} + +} // namespace SolveSpace diff --git a/src/handle.h b/src/handle.h new file mode 100644 index 0000000..e247e60 --- /dev/null +++ b/src/handle.h @@ -0,0 +1,47 @@ +#ifndef SOVLESPACE_HANDLE_H +#define SOVLESPACE_HANDLE_H + +#include +#include + +namespace SolveSpace { + +/// Trait indicating which types are handle types and should get the associated operators. +/// Specialize for each handle type and inherit from std::true_type. +template +struct IsHandleOracle : std::false_type {}; + +// Equality-compare any two instances of a handle type. +template +static inline typename std::enable_if::value, bool>::type +operator==(T const &lhs, T const &rhs) { + return lhs.v == rhs.v; +} + +// Inequality-compare any two instances of a handle type. +template +static inline typename std::enable_if::value, bool>::type +operator!=(T const &lhs, T const &rhs) { + return !(lhs == rhs); +} + +// Less-than-compare any two instances of a handle type. +template +static inline typename std::enable_if::value, bool>::type +operator<(T const &lhs, T const &rhs) { + return lhs.v < rhs.v; +} + +template +struct HandleHasher { + static_assert(IsHandleOracle::value, "Not a valid handle type"); + + inline size_t operator()(const T &h) const { + using Hasher = std::hash; + return Hasher{}(h.v); + } +}; + +} // namespace SolveSpace + +#endif // !SOVLESPACE_HANDLE_H diff --git a/src/importdxf.cpp b/src/importdxf.cpp new file mode 100644 index 0000000..ce80280 --- /dev/null +++ b/src/importdxf.cpp @@ -0,0 +1,1199 @@ +#include "solvespace.h" +#include "libdxfrw.h" +#include "libdwgr.h" + +namespace SolveSpace { + +static std::string ToUpper(std::string str) { + std::transform(str.begin(), str.end(), str.begin(), ::toupper); + return str; +} + +static Quaternion NormalFromExtPoint(Vector extPoint) { + // DXF arbitrary axis algorithm for transforming a Z-vector into a rotated + // coordinate system + Vector ax, ay; + Vector az = extPoint.WithMagnitude(1.0); + + if ((fabs(az.x) < 1/64.) && (fabs(az.y) < 1/64.)) { + ax = Vector::From(0, 1, 0).Cross(az).WithMagnitude(1.0); + } else { + ax = Vector::From(0, 0, 1).Cross(az).WithMagnitude(1.0); + } + ay = az.Cross(ax).WithMagnitude(1.0); + return Quaternion::From(ax, ay); +} + +class DxfImport : public DRW_Interface { +public: + Vector blockX; + Vector blockY; + Vector blockZ; + Vector blockT; + + void invertXTransform() { + blockX.x = -blockX.x; + blockY.x = -blockY.x; + blockT.x = -blockT.x; + } + + void multBlockTransform(double x, double y, double sx, double sy, double angle) { + Vector oldX = blockX; + Vector oldY = blockY; + Vector oldT = blockT; + + Vector newX = Vector::From(sx, 0.0, 0.0).RotatedAbout(Vector::From(0.0, 0.0, 1.0), angle); + Vector newY = Vector::From(0.0, sy, 0.0).RotatedAbout(Vector::From(0.0, 0.0, 1.0), angle); + Vector newT = Vector::From(x, y, 0.0); + + blockX = oldX.ScaledBy(newX.x).Plus( + oldY.ScaledBy(newX.y)); + + blockY = oldX.ScaledBy(newY.x).Plus( + oldY.ScaledBy(newY.y)); + + blockT = oldX.ScaledBy(newT.x).Plus( + oldY.ScaledBy(newT.y)).Plus(oldT); + } + + void clearBlockTransform() { + blockX = Vector::From(1.0, 0.0, 0.0); + blockY = Vector::From(0.0, 1.0, 0.0); + blockZ = Vector::From(0.0, 0.0, 1.0); + blockT = Vector::From(0.0, 0.0, 0.0); + } + + Vector blockTransform(Vector v) { + Vector r = blockT; + r = r.Plus(blockX.ScaledBy(v.x)); + r = r.Plus(blockY.ScaledBy(v.y)); + r = r.Plus(blockZ.ScaledBy(v.z)); + return r; + } + + void blockTransformArc(Vector *c, Vector *p0, Vector *p1) { + bool oldSign = p0->Minus(*c).Cross(p1->Minus(*c)).z > 0.0; + + *c = blockTransform(*c); + *p0 = blockTransform(*p0); + *p1 = blockTransform(*p1); + + bool newSign = p0->Minus(*c).Cross(p1->Minus(*c)).z > 0.0; + if(oldSign != newSign) std::swap(*p0, *p1); + } + + Vector toVector(const DRW_Coord &c, bool transform = true) { + Vector result = Vector::From(c.x, c.y, c.z); + if(transform) return blockTransform(result); + return result; + } + + Vector toVector(const DRW_Vertex2D &c) { + Vector result = Vector::From(c.x, c.y, 0.0); + return blockTransform(result); + } + + Vector toVector(const DRW_Vertex &c) { + Vector result = Vector::From(c.basePoint.x, c.basePoint.y, c.basePoint.z); + return blockTransform(result); + } + + double angleTo(Vector v0, Vector v1) { + Vector d = v1.Minus(v0); + double a = atan2(d.y, d.x); + return M_PI + remainder(a - M_PI, 2 * M_PI); + } + + Vector polar(double radius, double angle) { + return Vector::From(radius * cos(angle), radius * sin(angle), 0.0); + } + + hRequest createBulge(Vector p0, Vector p1, double bulge) { + bool reversed = bulge < 0.0; + double alpha = atan(bulge) * 4.0; + + Vector middle = p1.Plus(p0).ScaledBy(0.5); + double dist = p1.Minus(p0).Magnitude() / 2.0; + double angle = angleTo(p0, p1); + + // alpha can't be 0.0 at this point + double radius = fabs(dist / sin(alpha / 2.0)); + double wu = fabs(radius * radius - dist * dist); + double h = sqrt(wu); + + if(bulge > 0.0) { + angle += M_PI_2; + } else { + angle -= M_PI_2; + } + + if (fabs(alpha) > M_PI) { + h *= -1.0; + } + + Vector center = polar(h, angle); + center = center.Plus(middle); + + if(reversed) std::swap(p0, p1); + blockTransformArc(¢er, &p0, &p1); + + hRequest hr = SS.GW.AddRequest(Request::Type::ARC_OF_CIRCLE, /*rememberForUndo=*/false); + SK.GetEntity(hr.entity(1))->PointForceTo(center); + SK.GetEntity(hr.entity(2))->PointForceTo(p0); + SK.GetEntity(hr.entity(3))->PointForceTo(p1); + processPoint(hr.entity(1)); + processPoint(hr.entity(2)); + processPoint(hr.entity(3)); + return hr; + } + + struct Block { + std::vector> entities; + DRW_Block data; + }; + + bool asConstruction = false; + unsigned unknownEntities = 0; + std::map styles; + std::map blocks; + std::map layers; + Block *readBlock = NULL; + const DRW_Insert *insertInsert = NULL; + + template + bool addPendingBlockEntity(const T &e) { + if(readBlock == NULL) return false; + readBlock->entities.emplace_back(new T(e)); + return true; + } + + void addEntity(DRW_Entity *e) { + switch(e->eType) { + case DRW::POINT: + addPoint(*static_cast(e)); + break; + case DRW::LINE: + addLine(*static_cast(e)); + break; + case DRW::ARC: + addArc(*static_cast(e)); + break; + case DRW::CIRCLE: + addCircle(*static_cast(e)); + break; + case DRW::POLYLINE: + addPolyline(*static_cast(e)); + break; + case DRW::LWPOLYLINE: + addLWPolyline(*static_cast(e)); + break; + case DRW::SPLINE: + addSpline(static_cast(e)); + break; + case DRW::INSERT: + addInsert(*static_cast(e)); + break; + case DRW::TEXT: + addText(*static_cast(e)); + break; + case DRW::MTEXT: + addMText(*static_cast(e)); + break; + case DRW::DIMALIGNED: + addDimAlign(static_cast(e)); + break; + case DRW::DIMLINEAR: + addDimLinear(static_cast(e)); + break; + case DRW::DIMRADIAL: + addDimRadial(static_cast(e)); + break; + case DRW::DIMDIAMETRIC: + addDimDiametric(static_cast(e)); + break; + case DRW::DIMANGULAR: + addDimAngular(static_cast(e)); + break; + default: + unknownEntities++; + } + } + + Style::TextOrigin dxfAlignToOrigin(DRW_Text::HAlign alignH, DRW_Text::VAlign alignV) { + uint32_t origin = 0; + switch(alignH) { + case DRW_Text::HLeft: + origin |= (uint32_t)Style::TextOrigin::LEFT; + break; + + case DRW_Text::HMiddle: + case DRW_Text::HCenter: + break; + + case DRW_Text::HRight: + origin |= (uint32_t)Style::TextOrigin::RIGHT; + break; + + case DRW_Text::HAligned: + case DRW_Text::HFit: + default: + origin |= (uint32_t)Style::TextOrigin::LEFT; + break; + } + + switch(alignV) { + case DRW_Text::VBaseLine: + case DRW_Text::VBottom: + origin |= (uint32_t)Style::TextOrigin::BOT; + break; + + case DRW_Text::VMiddle: + break; + + case DRW_Text::VTop: + origin |= (uint32_t)Style::TextOrigin::TOP; + break; + + default: + origin |= (uint32_t)Style::TextOrigin::BOT; + break; + } + + return (Style::TextOrigin)origin; + } + + DRW_Layer *getSourceLayer(const DRW_Entity *e) { + DRW_Layer *layer = NULL; + if(insertInsert != NULL) { + std::string l = insertInsert->layer; + auto bi = layers.find(l); + if(bi != layers.end()) layer = &bi->second; + } else { + std::string l = e->layer; + auto bi = layers.find(l); + if(bi != layers.end()) layer = &bi->second; + } + return layer; + } + + int getColor(const DRW_Entity *e) { + int col = e->color; + if(col == DRW::ColorByBlock) { + if(insertInsert != NULL) { + col = insertInsert->color; + } else { + col = 7; + } + } + if(col == DRW::ColorByLayer) { + DRW_Layer *layer = getSourceLayer(e); + if(layer != NULL) { + col = layer->color; + } else { + col = 7; + } + } + return col; + } + + DRW_LW_Conv::lineWidth getLineWidth(const DRW_Entity *e) { + DRW_LW_Conv::lineWidth result = e->lWeight; + if(result == DRW_LW_Conv::widthByBlock) { + if(insertInsert != NULL) { + result = insertInsert->lWeight; + } else { + result = DRW_LW_Conv::widthDefault; + } + } + if(result == DRW_LW_Conv::widthByLayer) { + DRW_Layer *layer = getSourceLayer(e); + if(layer != NULL) { + result = layer->lWeight; + } else { + result = DRW_LW_Conv::widthDefault; + } + } + return result; + } + + std::string getLineType(const DRW_Entity *e) { + std::string result = e->lineType; + if(result == "BYBLOCK") { + if(insertInsert != NULL) { + result = ToUpper(insertInsert->lineType); + } else { + result = "CONTINUOUS"; + } + } + if(result == "BYLAYER") { + DRW_Layer *layer = getSourceLayer(e); + if(layer != NULL) { + result = ToUpper(layer->lineType); + } else { + result = "CONTINUOUS"; + } + } + return result; + } + + hStyle invisibleStyle() { + std::string id = "@dxf-invisible"; + + auto si = styles.find(id); + if(si != styles.end()) { + return si->second; + } + + hStyle hs = { Style::CreateCustomStyle(/*rememberForUndo=*/false) }; + Style *s = Style::Get(hs); + s->name = id; + s->visible = false; + + styles.emplace(id, hs); + return hs; + } + + hStyle styleFor(const DRW_Entity *e) { + // Color. + //! @todo which color to choose: index or RGB one? + int col = getColor(e); + RgbaColor c = RgbaColor::From(DRW::dxfColors[col][0], + DRW::dxfColors[col][1], + DRW::dxfColors[col][2]); + + // Line width. + DRW_LW_Conv::lineWidth lw = getLineWidth(e); + double width = DRW_LW_Conv::lineWidth2dxfInt(e->lWeight) / 100.0; + if(width < 0.0) width = 1.0; + + // Line stipple. + //! @todo Probably, we can load default autocad patterns and match it with ours. + std::string lineType = getLineType(e); + StipplePattern stipple = StipplePattern::CONTINUOUS; + for(uint32_t i = 0; i <= (uint32_t)StipplePattern::LAST; i++) { + StipplePattern st = (StipplePattern)i; + if(lineType == DxfFileWriter::lineTypeName(st)) { + stipple = st; + break; + } + } + + // Text properties. + DRW_Text::HAlign alignH = DRW_Text::HLeft; + DRW_Text::VAlign alignV = DRW_Text::VBaseLine; + double textAngle = 0.0; + double textHeight = Style::DefaultTextHeight(); + + if(e->eType == DRW::TEXT || e->eType == DRW::MTEXT) { + const DRW_Text *text = static_cast(e); + alignH = text->alignH; + alignV = text->alignV; + textHeight = text->height; + textAngle = text->angle; + // I have no idea why, but works + if(alignH == DRW_Text::HMiddle) { + alignV = DRW_Text::VMiddle; + } + } + + // Unique identifier based on style properties. + std::string id = "@dxf"; + if(lw != DRW_LW_Conv::widthDefault) + id += ssprintf("-w%.4g", width); + if(lineType != "CONTINUOUS") + id += ssprintf("-%s", lineType.c_str()); + if(c.red != 0 || c.green != 0 || c.blue != 0) + id += ssprintf("-#%02x%02x%02x", c.red, c.green, c.blue); + if(textHeight != Style::DefaultTextHeight()) + id += ssprintf("-h%.4g", textHeight); + if(textAngle != 0.0) + id += ssprintf("-a%.5g", textAngle); + if(alignH != DRW_Text::HLeft) + id += ssprintf("-oh%d", alignH); + if(alignV != DRW_Text::VBaseLine) + id += ssprintf("-ov%d", alignV); + + auto si = styles.find(id); + if(si != styles.end()) { + return si->second; + } + + hStyle hs = { Style::CreateCustomStyle(/*rememberForUndo=*/false) }; + Style *s = Style::Get(hs); + if(lw != DRW_LW_Conv::widthDefault) { + s->widthAs = Style::UnitsAs::MM; + s->width = width; + s->stippleScale = 1.0 + width * 2.0; + } + s->name = id; + s->stippleType = stipple; + if(c.red != 0 || c.green != 0 || c.blue != 0) s->color = c; + s->textHeightAs = Style::UnitsAs::MM; + s->textHeight = textHeight; + s->textAngle = textAngle; + s->textOrigin = dxfAlignToOrigin(alignH, alignV); + + styles.emplace(id, hs); + return hs; + } + + void configureRequest(hRequest hr, hStyle hs) { + Request *r = SK.GetRequest(hr); + r->construction = asConstruction; + r->style = hs; + } + + struct VectorHash { + size_t operator()(const Vector &v) const { + static const size_t size = std::numeric_limits::max() / 2 - 1; + static const double eps = (4.0 * LENGTH_EPS); + + double x = fabs(v.x) / eps; + double y = fabs(v.y) / eps; + + size_t xs = size_t(fmod(x, double(size))); + size_t ys = size_t(fmod(y, double(size))); + + return ys * size + xs; + } + }; + + struct VectorPred { + bool operator()(Vector a, Vector b) const { + return a.Equals(b, LENGTH_EPS); + } + }; + + std::unordered_map points; + + void processPoint(hEntity he, bool constrain = true) { + Entity *e = SK.GetEntity(he); + Vector pos = e->PointGetNum(); + hEntity p = findPoint(pos); + if(p == he) return; + if(p != Entity::NO_ENTITY) { + if(constrain) { + Constraint::ConstrainCoincident(he, p); + } + // We don't add point because we already + // have point in this position + return; + } + points.emplace(pos, he); + } + + hEntity findPoint(const Vector &p) { + auto it = points.find(p); + if(it == points.end()) return Entity::NO_ENTITY; + return it->second; + } + + hEntity createOrGetPoint(const Vector &p) { + hEntity he = findPoint(p); + if(he != Entity::NO_ENTITY) return he; + + hRequest hr = SS.GW.AddRequest(Request::Type::DATUM_POINT, /*rememberForUndo=*/false); + he = hr.entity(0); + SK.GetEntity(he)->PointForceTo(p); + points.emplace(p, he); + return he; + } + + hEntity createLine(Vector p0, Vector p1, hStyle style, bool constrainHV = false) { + if(p0.Equals(p1)) return Entity::NO_ENTITY; + hRequest hr = SS.GW.AddRequest(Request::Type::LINE_SEGMENT, /*rememberForUndo=*/false); + SK.GetEntity(hr.entity(1))->PointForceTo(p0); + SK.GetEntity(hr.entity(2))->PointForceTo(p1); + processPoint(hr.entity(1)); + processPoint(hr.entity(2)); + + if(constrainHV && SS.GW.LockedInWorkplane()) { + bool hasConstraint = false; + Constraint::Type cType; + if(fabs(p0.x - p1.x) < LENGTH_EPS) { + hasConstraint = true; + cType = Constraint::Type::VERTICAL; + } else if(fabs(p0.y - p1.y) < LENGTH_EPS) { + hasConstraint = true; + cType = Constraint::Type::HORIZONTAL; + } + if(hasConstraint) { + Constraint::Constrain( + cType, + Entity::NO_ENTITY, + Entity::NO_ENTITY, + hr.entity(0) + ); + } + } + + configureRequest(hr, style); + return hr.entity(0); + } + + hEntity createWorkplane(const Vector &p, const Quaternion &q) { + hRequest hr = SS.GW.AddRequest(Request::Type::WORKPLANE, /*rememberForUndo=*/false); + SK.GetEntity(hr.entity(1))->PointForceTo(p); + processPoint(hr.entity(1)); + SK.GetEntity(hr.entity(32))->NormalForceTo(q); + return hr.entity(0); + } + + hEntity findOrCreateWorkplane(const Vector &p, const Quaternion &q) { + Vector z = q.RotationN(); + for(auto &r : SK.request) { + if((r.type == Request::Type::WORKPLANE) && (r.group == SS.GW.activeGroup)) { + Vector wp = SK.GetEntity(r.h.entity(1))->PointGetNum(); + Vector wz = SK.GetEntity(r.h.entity(32))->NormalN(); + + if ((p.DistanceToPlane(wz, wp) < LENGTH_EPS) && z.Equals(wz)) { + return r.h.entity(0); + } + } + } + + return createWorkplane(p, q); + } + + static void activateWorkplane(hEntity he) { + Group *g = SK.GetGroup(SS.GW.activeGroup); + g->activeWorkplane = he; + } + + hEntity createCircle(const Vector &c, const Quaternion &q, double r, hStyle style) { + hRequest hr = SS.GW.AddRequest(Request::Type::CIRCLE, /*rememberForUndo=*/false); + SK.GetEntity(hr.entity(1))->PointForceTo(c); + processPoint(hr.entity(1)); + SK.GetEntity(hr.entity(32))->NormalForceTo(q); + SK.GetEntity(hr.entity(64))->DistanceForceTo(r); + configureRequest(hr, style); + return hr.entity(0); + } + + void addLayer(const DRW_Layer &data) override { + layers.emplace(data.name, data); + } + + void addBlock(const DRW_Block &data) override { + readBlock = &blocks[data.name]; + readBlock->data = data; + } + + void endBlock() override { + readBlock = NULL; + } + + void addPoint(const DRW_Point &data) override { + if(data.space != DRW::ModelSpace) return; + if(addPendingBlockEntity(data)) return; + + hRequest hr = SS.GW.AddRequest(Request::Type::DATUM_POINT, /*rememberForUndo=*/false); + SK.GetEntity(hr.entity(0))->PointForceTo(toVector(data.basePoint)); + processPoint(hr.entity(0)); + } + + void addLine(const DRW_Line &data) override { + if(data.space != DRW::ModelSpace) return; + if(addPendingBlockEntity(data)) return; + + createLine(toVector(data.basePoint), toVector(data.secPoint), styleFor(&data), + /*constrainHV=*/true); + } + + void addArc(const DRW_Arc &data) override { + if(data.space != DRW::ModelSpace) return; + if(addPendingBlockEntity(data)) return; + + double r = data.radious; + double sa = data.staangle; + double ea = data.endangle; + Vector c = toVector(data.basePoint); + Vector nz = toVector(data.extPoint); + Quaternion q = NormalFromExtPoint(nz); + + bool planar = q.RotationN().Equals(Vector::From(0, 0, 1)); + bool onPlane = c.z < LENGTH_EPS; + + hEntity oldWorkplane = SS.GW.ActiveWorkplane(); + if (!planar || !onPlane) { + activateWorkplane(findOrCreateWorkplane(c, q)); + } + + hRequest hr = SS.GW.AddRequest(Request::Type::ARC_OF_CIRCLE, /*rememberForUndo=*/false); + Vector u = q.RotationU(), v = q.RotationV(); + Vector rvs = c.Plus(u.ScaledBy(r * cos(sa))).Plus(v.ScaledBy(r * sin(sa))); + Vector rve = c.Plus(u.ScaledBy(r * cos(ea))).Plus(v.ScaledBy(r * sin(ea))); + + if(data.extPoint.z == -1.0) { + c.x = -c.x; + rvs.x = - rvs.x; + rve.x = - rve.x; + std::swap(rvs, rve); + } + + blockTransformArc(&c, &rvs, &rve); + + SK.GetEntity(hr.entity(1))->PointForceTo(c); + SK.GetEntity(hr.entity(2))->PointForceTo(rvs); + SK.GetEntity(hr.entity(3))->PointForceTo(rve); + processPoint(hr.entity(1)); + processPoint(hr.entity(2)); + processPoint(hr.entity(3)); + configureRequest(hr, styleFor(&data)); + activateWorkplane(oldWorkplane); + } + + void addCircle(const DRW_Circle &data) override { + if(data.space != DRW::ModelSpace) return; + if(addPendingBlockEntity(data)) return; + + Vector nz = toVector(data.extPoint); + Quaternion normal = NormalFromExtPoint(nz); + createCircle(toVector(data.basePoint), normal, data.radious, styleFor(&data)); + } + + void addLWPolyline(const DRW_LWPolyline &data) override { + if(data.space != DRW::ModelSpace) return; + if(addPendingBlockEntity(data)) return; + + size_t vNum = data.vertlist.size(); + + // Check for closed polyline. + if((data.flags & 1) != 1) vNum--; + + // Correct coordinate system for the case where z=-1, as described in + // http://paulbourke.net/dataformats/dxf/dxf10.html. + bool needSwapX = data.extPoint.z == -1.0; + + for(size_t i = 0; i < vNum; i++) { + DRW_Vertex2D c0 = *data.vertlist[i]; + DRW_Vertex2D c1 = *data.vertlist[(i + 1) % data.vertlist.size()]; + + if(needSwapX) { + c0.x = -c0.x; + c1.x = -c1.x; + c0.bulge = -c0.bulge; + } + + Vector p0 = Vector::From(c0.x, c0.y, 0.0); + Vector p1 = Vector::From(c1.x, c1.y, 0.0); + hStyle hs = styleFor(&data); + + if(EXACT(data.vertlist[i]->bulge == 0.0)) { + createLine(blockTransform(p0), blockTransform(p1), hs, /*constrainHV=*/true); + } else { + hRequest hr = createBulge(p0, p1, c0.bulge); + configureRequest(hr, hs); + } + } + } + + void addPolyline(const DRW_Polyline &data) override { + if(data.space != DRW::ModelSpace) return; + if(addPendingBlockEntity(data)) return; + + size_t vNum = data.vertlist.size(); + + // Check for closed polyline. + if((data.flags & 1) != 1) vNum--; + + // Correct coordinate system for the case where z=-1, as described in + // http://paulbourke.net/dataformats/dxf/dxf10.html. + bool needSwapX = (data.extPoint.z == -1.0); + + for(size_t i = 0; i < vNum; i++) { + DRW_Coord c0 = data.vertlist[i]->basePoint; + DRW_Coord c1 = data.vertlist[(i + 1) % data.vertlist.size()]->basePoint; + + double bulge = data.vertlist[i]->bulge; + if(needSwapX) { + c0.x = -c0.x; + c1.x = -c1.x; + bulge = -bulge; + } + + Vector p0 = Vector::From(c0.x, c0.y, c0.z); + Vector p1 = Vector::From(c1.x, c1.y, c1.z); + hStyle hs = styleFor(&data); + + if(EXACT(bulge == 0.0)) { + createLine(blockTransform(p0), blockTransform(p1), hs, /*constrainHV=*/true); + } else { + hRequest hr = createBulge(p0, p1, bulge); + configureRequest(hr, hs); + } + } + } + + void addSpline(const DRW_Spline *data) override { + if(data->space != DRW::ModelSpace) return; + if(data->degree != 3) return; + if(addPendingBlockEntity(*data)) return; + + hRequest hr = SS.GW.AddRequest(Request::Type::CUBIC, /*rememberForUndo=*/false); + for(int i = 0; i < 4; i++) { + SK.GetEntity(hr.entity(i + 1))->PointForceTo(toVector(*data->controllist[i])); + processPoint(hr.entity(i + 1)); + } + configureRequest(hr, styleFor(data)); + } + + void addInsert(const DRW_Insert &data) override { + if(data.space != DRW::ModelSpace) return; + if(addPendingBlockEntity(data)) return; + + auto bi = blocks.find(data.name); + ssassert(bi != blocks.end(), "Inserted block does not exist"); + Block *block = &bi->second; + + // Push transform. + Vector x = blockX; + Vector y = blockY; + Vector t = blockT; + + const DRW_Insert *oldInsert = insertInsert; + insertInsert = &data; + + if(data.extPoint.z == -1.0) invertXTransform(); + multBlockTransform(data.basePoint.x, data.basePoint.y, data.xscale, data.yscale, + data.angle); + for(auto &e : block->entities) { + addEntity(&*e); + } + + insertInsert = oldInsert; + + // Pop transform. + blockX = x; + blockY = y; + blockT = t; + } + + void addMText(const DRW_MText &data) override { + if(data.space != DRW::ModelSpace) return; + if(addPendingBlockEntity(data)) return; + + DRW_MText text = data; + text.secPoint = text.basePoint; + addText(text); + } + + void addText(const DRW_Text &data) override { + if(data.space != DRW::ModelSpace) return; + if(addPendingBlockEntity(data)) return; + + Constraint c = {}; + c.group = SS.GW.activeGroup; + c.workplane = SS.GW.ActiveWorkplane(); + c.type = Constraint::Type::COMMENT; + if(data.alignH == DRW_Text::HLeft && data.alignV == DRW_Text::VBaseLine) { + c.disp.offset = toVector(data.basePoint); + } else { + c.disp.offset = toVector(data.secPoint); + } + c.comment = data.text; + c.disp.style = styleFor(&data); + Constraint::AddConstraint(&c, /*rememberForUndo=*/false); + } + + void addDimAlign(const DRW_DimAligned *data) override { + if(data->space != DRW::ModelSpace) return; + if(addPendingBlockEntity(*data)) return; + + Vector p0 = toVector(data->getDef1Point()); + Vector p1 = toVector(data->getDef2Point()); + Vector p2 = toVector(data->getTextPoint()); + hConstraint hc = Constraint::Constrain( + Constraint::Type::PT_PT_DISTANCE, + createOrGetPoint(p0), + createOrGetPoint(p1), + Entity::NO_ENTITY + ); + + Constraint *c = SK.GetConstraint(hc); + if(data->hasActualMeasurement()) { + c->valA = data->getActualMeasurement(); + } else { + c->ModifyToSatisfy(); + } + c->disp.offset = p2.Minus(p0.Plus(p1).ScaledBy(0.5)); + } + + void addDimLinear(const DRW_DimLinear *data) override { + if(data->space != DRW::ModelSpace) return; + if(addPendingBlockEntity(*data)) return; + + Vector p0 = toVector(data->getDef1Point(), /*transform=*/false); + Vector p1 = toVector(data->getDef2Point(), /*transform=*/false); + Vector p2 = toVector(data->getTextPoint(), /*transform=*/false); + + double angle = data->getAngle() * PI / 180.0; + Vector dir = Vector::From(cos(angle), sin(angle), 0.0); + Vector p3 = p1.Minus(p1.ClosestPointOnLine(p2, dir)).Plus(p1); + if(p1.Minus(p3).Magnitude() < LENGTH_EPS) { + p3 = p0.Minus(p0.ClosestPointOnLine(p2, dir)).Plus(p1); + } + + Vector p4 = p0.ClosestPointOnLine(p1, p3.Minus(p1)).Plus(p0).ScaledBy(0.5); + + p0 = blockTransform(p0); + p1 = blockTransform(p1); + p2 = blockTransform(p2); + p3 = blockTransform(p3); + p4 = blockTransform(p4); + + hConstraint hc = Constraint::Constrain( + Constraint::Type::PT_LINE_DISTANCE, + createOrGetPoint(p0), + Entity::NO_ENTITY, + createLine(p1, p3, invisibleStyle()) + ); + + Constraint *c = SK.GetConstraint(hc); + if(data->hasActualMeasurement()) { + c->valA = data->getActualMeasurement(); + } else { + c->ModifyToSatisfy(); + } + c->disp.offset = p2.Minus(p4); + } + + void addDimAngular(const DRW_DimAngular *data) override { + if(data->space != DRW::ModelSpace) return; + if(addPendingBlockEntity(*data)) return; + + Vector l0p0 = toVector(data->getFirstLine1()); + Vector l0p1 = toVector(data->getFirstLine2()); + Vector l1p0 = toVector(data->getSecondLine1()); + Vector l1p1 = toVector(data->getSecondLine2()); + + hConstraint hc = Constraint::Constrain( + Constraint::Type::ANGLE, + Entity::NO_ENTITY, + Entity::NO_ENTITY, + createLine(l0p0, l0p1, invisibleStyle()), + createLine(l1p1, l1p0, invisibleStyle()), + /*other=*/false, + /*other2=*/false + ); + + Constraint *c = SK.GetConstraint(hc); + c->ModifyToSatisfy(); + if(data->hasActualMeasurement()) { + double actual = data->getActualMeasurement() / PI * 180.0; + if(fabs(180.0 - actual - c->valA) < fabs(actual - c->valA)) { + c->other = true; + } + c->valA = actual; + } + + bool skew = false; + Vector pi = Vector::AtIntersectionOfLines(l0p0, l0p1, l1p0, l1p1, &skew); + if(!skew) { + c->disp.offset = toVector(data->getTextPoint()).Minus(pi); + } + } + + hConstraint createDiametric(Vector cp, Quaternion q, double r, Vector tp, + double actual, bool asRadius = false) { + hEntity he = createCircle(cp, q, r, invisibleStyle()); + + hConstraint hc = Constraint::Constrain( + Constraint::Type::DIAMETER, + Entity::NO_ENTITY, + Entity::NO_ENTITY, + he + ); + + Constraint *c = SK.GetConstraint(hc); + if(actual > 0.0) { + c->valA = asRadius ? actual * 2.0 : actual; + } else { + c->ModifyToSatisfy(); + } + c->disp.offset = tp.Minus(cp); + if(asRadius) c->other = true; + return hc; + } + + void addDimRadial(const DRW_DimRadial *data) override { + if(data->space != DRW::ModelSpace) return; + if(addPendingBlockEntity(*data)) return; + + Vector cp = toVector(data->getCenterPoint()); + Vector dp = toVector(data->getDiameterPoint()); + Vector tp = toVector(data->getTextPoint()); + double actual = -1.0; + if(data->hasActualMeasurement()) { + actual = data->getActualMeasurement(); + } + + Vector nz = toVector(data->getExtrusion()); + Quaternion q = NormalFromExtPoint(nz); + createDiametric(cp, q, cp.Minus(dp).Magnitude(), tp, actual, /*asRadius=*/true); + } + + void addDimDiametric(const DRW_DimDiametric *data) override { + if(data->space != DRW::ModelSpace) return; + if(addPendingBlockEntity(*data)) return; + + Vector dp1 = toVector(data->getDiameter1Point()); + Vector dp2 = toVector(data->getDiameter2Point()); + + Vector cp = dp1.Plus(dp2).ScaledBy(0.5); + Vector tp = toVector(data->getTextPoint()); + double actual = -1.0; + if(data->hasActualMeasurement()) { + actual = data->getActualMeasurement(); + } + + Vector nz = toVector(data->getExtrusion()); + Quaternion q = NormalFromExtPoint(nz); + createDiametric(cp, q, cp.Minus(dp1).Magnitude(), tp, actual, /*asRadius=*/false); + } + + void addDimAngular3P(const DRW_DimAngular3p *data) override { + if(data->space != DRW::ModelSpace) return; + if(addPendingBlockEntity(*data)) return; + + DRW_DimAngular dim = *static_cast(data); + dim.setFirstLine1(data->getVertexPoint()); + dim.setFirstLine2(data->getFirstLine()); + dim.setSecondLine1(data->getVertexPoint()); + dim.setSecondLine2(data->getSecondLine()); + addDimAngular(&dim); + } +}; + +class DxfCheck3D : public DRW_Interface { +public: + bool is3d; + + void addEntity(DRW_Entity *e) { + switch(e->eType) { + case DRW::POINT: + addPoint(*static_cast(e)); + break; + case DRW::LINE: + addLine(*static_cast(e)); + break; + case DRW::ARC: + addArc(*static_cast(e)); + break; + case DRW::CIRCLE: + addCircle(*static_cast(e)); + break; + case DRW::POLYLINE: + addPolyline(*static_cast(e)); + break; + case DRW::LWPOLYLINE: + addLWPolyline(*static_cast(e)); + break; + case DRW::SPLINE: + addSpline(static_cast(e)); + break; + case DRW::INSERT: + addInsert(*static_cast(e)); + break; + case DRW::TEXT: + addText(*static_cast(e)); + break; + case DRW::MTEXT: + addMText(*static_cast(e)); + break; + case DRW::DIMALIGNED: + addDimAlign(static_cast(e)); + break; + case DRW::DIMLINEAR: + addDimLinear(static_cast(e)); + break; + case DRW::DIMRADIAL: + addDimRadial(static_cast(e)); + break; + case DRW::DIMDIAMETRIC: + addDimDiametric(static_cast(e)); + break; + case DRW::DIMANGULAR: + addDimAngular(static_cast(e)); + break; + default: + break; + } + } + + void addPoint(const DRW_Point &data) override { + if(data.space != DRW::ModelSpace) return; + checkCoord(data.basePoint); + } + + void addLine(const DRW_Line &data) override { + if(data.space != DRW::ModelSpace) return; + checkCoord(data.basePoint); + checkCoord(data.secPoint); + } + + void addArc(const DRW_Arc &data) override { + if(data.space != DRW::ModelSpace) return; + checkCoord(data.basePoint); + checkExt(data.extPoint); + } + + void addCircle(const DRW_Circle &data) override { + if(data.space != DRW::ModelSpace) return; + checkCoord(data.basePoint); + checkExt(data.extPoint); + } + + void addPolyline(const DRW_Polyline &data) override { + if(data.space != DRW::ModelSpace) return; + for(size_t i = 0; i < data.vertlist.size(); i++) { + checkCoord(data.vertlist[i]->basePoint); + } + } + + void addSpline(const DRW_Spline *data) override { + if(data->space != DRW::ModelSpace) return; + if(data->degree != 3) return; + for(int i = 0; i < 4; i++) { + checkCoord(*data->controllist[i]); + } + } + + void addInsert(const DRW_Insert &data) override { + if(data.space != DRW::ModelSpace) return; + checkCoord(data.basePoint); + } + + void addMText(const DRW_MText &data) override { + if(data.space != DRW::ModelSpace) return; + + DRW_MText text = data; + text.secPoint = text.basePoint; + addText(text); + } + + void addText(const DRW_Text &data) override { + if(data.space != DRW::ModelSpace) return; + checkCoord(data.basePoint); + checkCoord(data.secPoint); + } + + void addDimAlign(const DRW_DimAligned *data) override { + if(data->space != DRW::ModelSpace) return; + checkCoord(data->getDef1Point()); + checkCoord(data->getDef2Point()); + checkCoord(data->getTextPoint()); + } + + void addDimLinear(const DRW_DimLinear *data) override { + if(data->space != DRW::ModelSpace) return; + checkCoord(data->getDef1Point()); + checkCoord(data->getDef2Point()); + checkCoord(data->getTextPoint()); + } + + void addDimAngular(const DRW_DimAngular *data) override { + if(data->space != DRW::ModelSpace) return; + checkCoord(data->getFirstLine1()); + checkCoord(data->getFirstLine2()); + checkCoord(data->getSecondLine1()); + checkCoord(data->getSecondLine2()); + checkCoord(data->getTextPoint()); + } + + void addDimRadial(const DRW_DimRadial *data) override { + if(data->space != DRW::ModelSpace) return; + checkCoord(data->getCenterPoint()); + checkCoord(data->getDiameterPoint()); + checkCoord(data->getTextPoint()); + checkExt(data->getExtrusion()); + } + + void addDimDiametric(const DRW_DimDiametric *data) override { + if(data->space != DRW::ModelSpace) return; + checkCoord(data->getDiameter1Point()); + checkCoord(data->getDiameter2Point()); + checkCoord(data->getTextPoint()); + checkExt(data->getExtrusion()); + } + + void addDimAngular3P(const DRW_DimAngular3p *data) override { + if(data->space != DRW::ModelSpace) return; + DRW_DimAngular dim = *static_cast(data); + + dim.setFirstLine1(data->getVertexPoint()); + dim.setFirstLine2(data->getFirstLine()); + dim.setSecondLine1(data->getVertexPoint()); + dim.setSecondLine2(data->getSecondLine()); + addDimAngular(&dim); + } + + void checkCoord(const DRW_Coord &coord) { + if(fabs(coord.z) > LENGTH_EPS) { + is3d = true; + } + } + + void checkExt(const DRW_Coord &coord) { + if ((fabs(coord.x) > 1/64.) || (fabs(coord.y) > 1/64.)) { + is3d = true; + } + } +}; + +static void +ImportDwgDxf(const Platform::Path &filename, + const std::function &read) { + std::string fileType = ToUpper(filename.Extension()); + + std::string data; + if(!ReadFile(filename, &data)) { + Error("Couldn't read from '%s'", filename.raw.c_str()); + return; + } + + bool asConstruction = true; + if(SS.GW.LockedInWorkplane()) { + DxfCheck3D checker = {}; + read(data, &checker); + if(checker.is3d) { + Message("This %s file contains entities with non-zero Z coordinate; " + "the entire file will be imported as construction entities in 3d.", + fileType.c_str()); + SS.GW.SetWorkplaneFreeIn3d(); + SS.GW.EnsureValidActives(); + } else { + asConstruction = false; + } + } + + SS.UndoRemember(); + + DxfImport importer = {}; + importer.asConstruction = asConstruction; + importer.clearBlockTransform(); + if(!read(data, &importer)) { + Error("Corrupted %s file.", fileType.c_str()); + return; + } + if(importer.unknownEntities > 0) { + Message("%u %s entities of unknown type were ignored.", + importer.unknownEntities, fileType.c_str()); + } +} + +void ImportDxf(const Platform::Path &filename) { + ImportDwgDxf(filename, [](const std::string &data, DRW_Interface *intf) { + std::stringstream stream(data); + return dxfRW().read(stream, intf, /*ext=*/true); + }); +} + +void ImportDwg(const Platform::Path &filename) { + ImportDwgDxf(filename, [](const std::string &data, DRW_Interface *intf) { + std::stringstream stream(data); + return dwgR().read(stream, intf, /*ext=*/true); + }); +} + +} diff --git a/src/importidf.cpp b/src/importidf.cpp new file mode 100644 index 0000000..5523cab --- /dev/null +++ b/src/importidf.cpp @@ -0,0 +1,536 @@ +//----------------------------------------------------------------------------- +// Intermediate Data Format (IDF) file reader. Reads an IDF file for PCB outlines and creates +// an equivalent SovleSpace sketch/extrusion. Supports only Linking, not import. +// Part placement is not currently supported. +// +// Copyright 2020 Paul Kahler. +//----------------------------------------------------------------------------- +#include "solvespace.h" +#include "sketch.h" + +namespace SolveSpace { + +// Split a string into substrings separated by spaces. +// Allow quotes to enclose spaces within a string +static std::vector splitString(const std::string line) { + std::vector v = {}; + + if(line.length() == 0) return v; + + std::string s = ""; + bool inString = false; + bool inQuotes = false; + + for (size_t i=0; i 0) + v.push_back(s); + + return v; +} + +static bool isHoleDuplicate(EntityList *el, double x, double y, double r) { + bool duplicate = false; + for(int i = 0; i < el->n && !duplicate; i++) { + Entity &en = el->Get(i); + if(en.type != Entity::Type::CIRCLE) + continue; + Entity *distance = el->FindById(en.distance); + Entity *center = el->FindById(en.point[0]); + duplicate = + center->actPoint.x == x && center->actPoint.y == y && distance->actDistance == r; + } + return duplicate; +} + +////////////////////////////////////////////////////////////////////////////// +// Functions for linking an IDF file - we need to create entities that +// get remapped into a linked group similar to linking .slvs files +////////////////////////////////////////////////////////////////////////////// + +// Make a new point - type doesn't matter since we will make a copy later +static hEntity newPoint(EntityList *el, int *id, Vector p, bool visible = true) { + Entity en = {}; + en.type = Entity::Type::POINT_N_COPY; + en.extraPoints = 0; + en.timesApplied = 0; + en.group.v = 462; + en.actPoint = p; + en.construction = false; + en.style.v = Style::DATUM; + en.actVisible = visible; + en.forceHidden = false; + + *id = *id+1; + en.h.v = *id + en.group.v*65536; + el->Add(&en); + return en.h; +} + +static hEntity newLine(EntityList *el, int *id, hEntity p0, hEntity p1, bool keepout) { + Entity en = {}; + en.type = Entity::Type::LINE_SEGMENT; + en.point[0] = p0; + en.point[1] = p1; + en.extraPoints = 0; + en.timesApplied = 0; + en.group.v = 493; + en.construction = keepout; + en.style.v = keepout? Style::CONSTRUCTION : Style::ACTIVE_GRP; + en.actVisible = true; + en.forceHidden = false; + + *id = *id+1; + en.h.v = *id + en.group.v*65536; + el->Add(&en); + return en.h; +} + +static hEntity newNormal(EntityList *el, int *id, Quaternion normal) { + // normals have parameters, but we don't need them to make a NORMAL_N_COPY from this + Entity en = {}; + en.type = Entity::Type::NORMAL_N_COPY; + en.extraPoints = 0; + en.timesApplied = 0; + en.group.v = 472; + en.actNormal = normal; + en.construction = false; + en.style.v = Style::ACTIVE_GRP; + // to be visible we need to add a point. + en.point[0] = newPoint(el, id, Vector::From(0,0,3), /*visible=*/ true); + en.actVisible = true; + en.forceHidden = false; + + *id = *id+1; + en.h.v = *id + en.group.v*65536; + el->Add(&en); + return en.h; +} + +static hEntity newArc(EntityList *el, int *id, hEntity p0, hEntity p1, hEntity pc, hEntity hnorm, bool keepout) { + Entity en = {}; + en.type = Entity::Type::ARC_OF_CIRCLE; + en.point[0] = pc; + en.point[1] = p0; + en.point[2] = p1; + en.normal = hnorm; + en.extraPoints = 0; + en.timesApplied = 0; + en.group.v = 403; + en.construction = keepout; + en.style.v = keepout? Style::CONSTRUCTION : Style::ACTIVE_GRP; + en.actVisible = true; + en.forceHidden = false; *id = *id+1; + + *id = *id + 1; + en.h.v = *id + en.group.v*65536; + el->Add(&en); + return en.h; +} + +static hEntity newDistance(EntityList *el, int *id, double distance) { + // normals have parameters, but we don't need them to make a NORMAL_N_COPY from this + Entity en = {}; + en.type = Entity::Type::DISTANCE; + en.extraPoints = 0; + en.timesApplied = 0; + en.group.v = 472; + en.actDistance = distance; + en.construction = false; + en.style.v = Style::ACTIVE_GRP; + // to be visible we'll need to add a point? + en.actVisible = false; + en.forceHidden = false; + + *id = *id+1; + en.h.v = *id + en.group.v*65536; + el->Add(&en); + return en.h; +} + +static hEntity newCircle(EntityList *el, int *id, hEntity p0, hEntity hdist, hEntity hnorm, bool keepout) { + Entity en = {}; + en.type = Entity::Type::CIRCLE; + en.point[0] = p0; + en.normal = hnorm; + en.distance = hdist; + en.extraPoints = 0; + en.timesApplied = 0; + en.group.v = 399; + en.construction = keepout; + en.style.v = keepout? Style::CONSTRUCTION : Style::ACTIVE_GRP; + en.actVisible = true; + en.forceHidden = false; + + *id = *id+1; + en.h.v = *id + en.group.v*65536; + el->Add(&en); + return en.h; +} + +static Vector ArcCenter(Vector p0, Vector p1, double angle) { + // locate the center of an arc + Vector m = p0.Plus(p1).ScaledBy(0.5); + Vector perp = Vector::From(p1.y-p0.y, p0.x-p1.x, 0.0).WithMagnitude(1.0); + double dist = 0; + if (angle != 180) { + dist = (p1.Minus(m).Magnitude())/tan(0.5*angle*3.141592653589793/180.0); + } else { + dist = 0.0; + } + Vector c = m.Minus(perp.ScaledBy(dist)); + return c; +} + +// Add an IDF line or arc to the entity list. According to spec, zero angle indicates a line. +// Positive angles are counter clockwise, negative are clockwise. An angle of 360 +// indicates a circle centered at x1,y1 passing through x2,y2 and is a complete loop. +static void CreateEntity(EntityList *el, int *id, hEntity h0, hEntity h1, hEntity hnorm, + Vector p0, Vector p1, double angle, bool keepout) { + if (fabs(angle) < 0.1) { + //line + if(p0.Equals(p1)) return; + + newLine(el, id, h0, h1, keepout); + + } else if(angle == 360.0) { + // circle + double d = p1.Minus(p0).Magnitude(); + hEntity hd = newDistance(el, id, d); + newCircle(el, id, h1, hd, hnorm, keepout); + + } else { + // arc + if(angle < 0.0) { + swap(p0,p1); + swap(h0,h1); + angle = fabs(angle); + } + // locate the center of the arc + Vector m = p0.Plus(p1).ScaledBy(0.5); + Vector perp = Vector::From(p1.y-p0.y, p0.x-p1.x, 0.0).WithMagnitude(1.0); + // half angle in radians + double theta = 0.5*angle*PI/180.0; + double dist = (p1.Minus(m).Magnitude())*cos(theta)/sin(theta); + Vector c = m.Minus(perp.ScaledBy(dist)); + hEntity hc = newPoint(el, id, c, /*visible=*/false); + newArc(el, id, h0, h1, hc, hnorm, keepout); + } +} + +// borrowed from Entity::GenerateBezierCurves because we don't have parameters. +static void MakeBeziersForArcs(SBezierList *sbl, Vector center, Vector pa, Vector pb, + Quaternion q, double angle) { + + Vector u = q.RotationU(), v = q.RotationV(); + double r = pa.Minus(center).Magnitude(); + double theta, dtheta; + + if(angle == 360.0) { + theta = 0; + } else { + Point2d c2 = center.Project2d(u, v); + Point2d pa2 = (pa.Project2d(u, v)).Minus(c2); + + theta = atan2(pa2.y, pa2.x); + } + dtheta = angle * PI/180; + + int i, n; + if(dtheta > (3*PI/2 + 0.01)) { + n = 4; + } else if(dtheta > (PI + 0.01)) { + n = 3; + } else if(dtheta > (PI/2 + 0.01)) { + n = 2; + } else { + n = 1; + } + dtheta /= n; + + for(i = 0; i < n; i++) { + double s, c; + + c = cos(theta); + s = sin(theta); + // The start point of the curve, and the tangent vector at + // that start point. + Vector p0 = center.Plus(u.ScaledBy( r*c)).Plus(v.ScaledBy(r*s)), + t0 = u.ScaledBy(-r*s). Plus(v.ScaledBy(r*c)); + + theta += dtheta; + + c = cos(theta); + s = sin(theta); + Vector p2 = center.Plus(u.ScaledBy( r*c)).Plus(v.ScaledBy(r*s)), + t2 = u.ScaledBy(-r*s). Plus(v.ScaledBy(r*c)); + + // The control point must lie on both tangents. + Vector p1 = Vector::AtIntersectionOfLines(p0, p0.Plus(t0), + p2, p2.Plus(t2), + NULL); + + SBezier sb = SBezier::From(p0, p1, p2); + sb.weight[1] = cos(dtheta/2); + sbl->l.Add(&sb); + } +} + +// Here we read the important section of an IDF file. SolveSpace Entities are directly created by +// the functions above, which is only OK because of the way linking works. For example points do +// not have handles for solver parameters (coordinates), they only have their actPoint values +// set (or actNormal or actDistance). These are incomplete entities and would be a problem if +// they were part of the sketch, but they are not. After making a list of them here, a new group +// gets created from copies of these. Those copies are complete and part of the sketch group. +bool LinkIDF(const Platform::Path &filename, EntityList *el, SMesh *m, SShell *sh) { + dbp("\nLink IDF board outline."); + el->Clear(); + std::string data; + if(!ReadFile(filename, &data)) { + Error("Couldn't read from '%s'", filename.raw.c_str()); + return false; + } + + enum IDF_SECTION { + none, + header, + board_outline, + other_outline, + routing_outline, + placement_outline, + routing_keepout, + via_keepout, + placement_group, + drilled_holes, + notes, + component_placement + } section; + + section = IDF_SECTION::none; + int record_number = 0; + int curve = -1; + int entityCount = 0; + + hEntity hprev; + hEntity hprevTop; + Vector pprev = Vector::From(0,0,0); + Vector pprevTop = Vector::From(0,0,0); + + double board_thickness = 10.0; + double scale = 1.0; //mm + bool topEntities = false; + bool bottomEntities = false; + + Quaternion normal = Quaternion::From(Vector::From(1,0,0), Vector::From(0,1,0)); + hEntity hnorm = newNormal(el, &entityCount, normal); + + // to create the extursion we will need to collect a set of bezier curves defined + // by the perimeter, cutouts, and holes. + SBezierList sbl = {}; + + std::stringstream stream(data); + for(std::string line; getline( stream, line ); ) { + if (line.find(".END_") == 0) { + section = none; + curve = -1; + } + switch (section) { + case none: + if(line.find(".HEADER") == 0) { + section = header; + record_number = 1; + } else if (line.find(".BOARD_OUTLINE") == 0) { + section = board_outline; + record_number = 1; + } else if (line.find(".ROUTE_KEEPOUT") == 0) { + section = routing_keepout; + record_number = 1; + } else if(line.find(".DRILLED_HOLES") == 0) { + section = drilled_holes; + record_number = 1; + } + break; + + case header: + if(record_number == 3) { + if(line.find("MM") != std::string::npos) { + dbp("IDF units are MM"); + scale = 1.0; + } else if(line.find("THOU") != std::string::npos) { + dbp("IDF units are thousandths of an inch"); + scale = 0.0254; + } else { + dbp("IDF import, no units found in file."); + } + } + break; + + case routing_keepout: + case board_outline: + if (record_number == 2) { + if(section == board_outline) { + topEntities = true; + bottomEntities = true; + board_thickness = std::stod(line) * scale; + dbp("IDF board thickness: %lf", board_thickness); + } else if (section == routing_keepout) { + topEntities = false; + bottomEntities = false; + if(line.find("TOP") == 0 || line.find("BOTH") == 0) + topEntities = true; + if(line.find("BOTTOM") == 0 || line.find("BOTH") == 0) + bottomEntities = true; + } + } else { // records 3+ are lines, arcs, and circles + std::vector values = splitString(line); + if(values.size() != 4) continue; + int c = stoi(values[0]); + double x = stof(values[1]) * scale; + double y = stof(values[2]) * scale; + double ang = stof(values[3]); + Vector point = Vector::From(x,y,0.0); + Vector pTop = Vector::From(x,y,board_thickness); + if(c != curve) { // start a new curve + curve = c; + if (bottomEntities) + hprev = newPoint(el, &entityCount, point, /*visible=*/false); + if (topEntities) + hprevTop = newPoint(el, &entityCount, pTop, /*visible=*/false); + pprev = point; + pprevTop = pTop; + } else { + if(section == board_outline) { + // create a bezier for the extrusion + if (ang == 0) { + // straight lines + SBezier sb = SBezier::From(pprev, point); + sbl.l.Add(&sb); + } else if (ang != 360.0) { + // Arcs + Vector c = ArcCenter(pprev, point, ang); + MakeBeziersForArcs(&sbl, c, pprev, point, normal, ang); + } else { + // circles + MakeBeziersForArcs(&sbl, point, pprev, pprev, normal, ang); + } + } + // next create the entities + // only curves and points at circle centers will be visible + bool vis = (ang == 360.0); + if (bottomEntities) { + hEntity hp = newPoint(el, &entityCount, point, /*visible=*/vis); + CreateEntity(el, &entityCount, hprev, hp, hnorm, pprev, point, ang, + (section == routing_keepout) ); + pprev = point; + hprev = hp; + } + if (topEntities) { + hEntity hp = newPoint(el, &entityCount, pTop, /*visible=*/vis); + CreateEntity(el, &entityCount, hprevTop, hp, hnorm, pprevTop, pTop, + ang, (section == routing_keepout) ); + pprevTop = pTop; + hprevTop = hp; + } + } + } + break; + + case other_outline: + case routing_outline: + case placement_outline: + case via_keepout: + case placement_group: + break; + + case drilled_holes: { + std::vector values = splitString(line); + if(values.size() < 6) continue; + double d = stof(values[0]) * scale; + double x = stof(values[1]) * scale; + double y = stof(values[2]) * scale; + bool duplicate = isHoleDuplicate(el, x, y, d / 2); + // Only show holes likely to be useful in MCAD to reduce complexity. + if(((d > 1.7) || (values[5].compare(0,3,"PIN") == 0) + || (values[5].compare(0,3,"MTG") == 0)) && !duplicate) { + // create the entity + Vector cent = Vector::From(x,y,0.0); + hEntity hcent = newPoint(el, &entityCount, cent); + hEntity hdist = newDistance(el, &entityCount, d/2); + newCircle(el, &entityCount, hcent, hdist, hnorm, false); + // and again for the top + Vector cTop = Vector::From(x,y,board_thickness); + hcent = newPoint(el, &entityCount, cTop); + hdist = newDistance(el, &entityCount, d/2); + newCircle(el, &entityCount, hcent, hdist, hnorm, false); + // create the curves for the extrusion + Vector pt = Vector::From(x+d/2, y, 0.0); + MakeBeziersForArcs(&sbl, cent, pt, pt, normal, 360.0); + } + + break; + } + case notes: + case component_placement: + break; + + default: + section = none; + break; + } + record_number++; + } + // now we can create an extrusion from all the Bezier curves. We can skip things + // like checking for a coplanar sketch because everything is at z=0. + SPolygon polyLoops = {}; + bool allClosed; + bool allCoplanar; + Vector errorPointAt = Vector::From(0,0,0); + SEdge errorAt = {}; + + SBezierLoopSetSet sblss = {}; + sblss.FindOuterFacesFrom(&sbl, &polyLoops, NULL, + 100.0, &allClosed, &errorAt, + &allCoplanar, &errorPointAt, NULL); + + //hack for when there is no sketch yet and the first group is a linked IDF + double ctc = SS.chordTolCalculated; + if(ctc == 0.0) SS.chordTolCalculated = 0.1; //mm + // there should only by one sbls in the sblss unless a board has disjointed parts... + sh->MakeFromExtrusionOf(sblss.l.First(), Vector::From(0.0, 0.0, 0.0), + Vector::From(0.0, 0.0, board_thickness), + RgbaColor::From(0, 180, 0) ); + SS.chordTolCalculated = ctc; + sblss.Clear(); + sbl.Clear(); + sh->booleanFailed = false; + + return true; +} + +} // namespace SolveSpace diff --git a/src/importmesh.cpp b/src/importmesh.cpp new file mode 100644 index 0000000..fc1a4ff --- /dev/null +++ b/src/importmesh.cpp @@ -0,0 +1,258 @@ +//----------------------------------------------------------------------------- +// Triangle mesh file reader. Reads an STL file triangle mesh and creates +// a SovleSpace SMesh from it. Supports only Linking, not import. +// +// Copyright 2020 Paul Kahler. +//----------------------------------------------------------------------------- +#include "solvespace.h" +#include "sketch.h" +#include +#include + +#define MIN_POINT_DISTANCE 0.001 + +namespace SolveSpace { + +// we will check for duplicate vertices and keep all their normals +class vertex { +public: + Vector p; + std::vector normal; +}; + +static bool isEdgeVertex(vertex &v) { + unsigned int i,j; + bool result = false; + for(i=0;i &lv, Vector &p, Vector &n) { + unsigned int i; + for(i=0; iAdd(&en); + return en.h; +} + +// check if a vertex is unique and add it via newPoint if it is. +static void addVertex(EntityList *el, Vector v) { + if(el->n < 15000) { + int id = el->n; + newPoint(el, &id, v); + } +} + +static hEntity newNormal(EntityList *el, int *id, Quaternion normal, hEntity p) { + // normals have parameters, but we don't need them to make a NORMAL_N_COPY from this + Entity en = {}; + en.type = Entity::Type::NORMAL_N_COPY; + en.extraPoints = 0; + en.timesApplied = 0; + en.group.v = 472; + en.actNormal = normal; + en.construction = false; + en.style.v = Style::NORMALS; + // to be visible we need to add a point. +// en.point[0] = newPoint(el, id, Vector::From(0,0,0)); + en.point[0] = p; + en.actVisible = true; + en.forceHidden = false; + + *id = *id+1; + en.h.v = *id + en.group.v*65536; + el->Add(&en); + return en.h; +} + +static hEntity newLine(EntityList *el, int *id, hEntity p0, hEntity p1) { + Entity en = {}; + en.type = Entity::Type::LINE_SEGMENT; + en.point[0] = p0; + en.point[1] = p1; + en.extraPoints = 0; + en.timesApplied = 0; + en.group.v = 493; + en.construction = true; + en.style.v = Style::CONSTRUCTION; + en.actVisible = true; + en.forceHidden = false; + + en.h.v = *id + en.group.v*65536; + *id = *id + 1; + el->Add(&en); + return en.h; +} + +bool LinkStl(const Platform::Path &filename, EntityList *el, SMesh *m, SShell *sh) { + dbp("\nLink STL triangle mesh."); + el->Clear(); + std::string data; + if(!ReadFile(filename, &data)) { + Error("Couldn't read from '%s'", filename.raw.c_str()); + return false; + } + + std::stringstream f(data); + + char str[80] = {}; + f.read(str, 80); + + if(0==memcmp("solid", str, 5)) { + // just returning false will trigger the warning that linked file is not present + // best solution is to add an importer for text STL. + Message(_("Text-formated STL files are not currently supported")); + return false; + } + + uint32_t n; + uint32_t color; + + f.read((char*)&n, 4); + dbp("%d triangles", n); + + float x,y,z; + float xn,yn,zn; + + std::vector verts = {}; + + for(uint32_t i = 0; i> 7) & 0xf8; + tr.meta.color.green = (color >> 2) & 0xf8; + tr.meta.color.blue = (color << 3); + tr.meta.color.alpha = 255; + } else { + tr.meta.color.red = 90; + tr.meta.color.green = 120; + tr.meta.color.blue = 140; + tr.meta.color.alpha = 255; + } + + m->AddTriangle(&tr); + Vector normal = tr.Normal().WithMagnitude(1.0); + addUnique(verts, tr.a, normal); + addUnique(verts, tr.b, normal); + addUnique(verts, tr.c, normal); + } + dbp("%d vertices", verts.size()); + + int id = 1; + + //add the STL origin and normals + hEntity origin = newPoint(el, &id, Vector::From(0.0, 0.0, 0.0)); + newNormal(el, &id, Quaternion::From(Vector::From(1,0,0),Vector::From(0,1,0)), origin); + newNormal(el, &id, Quaternion::From(Vector::From(0,1,0),Vector::From(0,0,1)), origin); + newNormal(el, &id, Quaternion::From(Vector::From(0,0,1),Vector::From(1,0,0)), origin); + + BBox box = {}; + box.minp = verts[0].p; + box.maxp = verts[0].p; + + // determine the bounding box for all vertexes + for(unsigned int i=1; i + +namespace SolveSpace { + +void SMesh::Clear() { + l.Clear(); +} + +void SMesh::AddTriangle(STriMeta meta, Vector n, Vector a, Vector b, Vector c) { + Vector ab = b.Minus(a), bc = c.Minus(b); + Vector np = ab.Cross(bc); + if(np.Magnitude() < 1e-10) { + // ugh; gl sometimes tessellates to collinear triangles + return; + } + if(np.Dot(n) > 0) { + AddTriangle(meta, a, b, c); + } else { + AddTriangle(meta, c, b, a); + } +} +void SMesh::AddTriangle(STriMeta meta, Vector a, Vector b, Vector c) { + STriangle t = {}; + t.meta = meta; + t.a = a; + t.b = b; + t.c = c; + AddTriangle(&t); +} +void SMesh::AddTriangle(const STriangle *st) { + l.Add(st); +} + +void SMesh::DoBounding(Vector v, Vector *vmax, Vector *vmin) const { + vmax->x = max(vmax->x, v.x); + vmax->y = max(vmax->y, v.y); + vmax->z = max(vmax->z, v.z); + + vmin->x = min(vmin->x, v.x); + vmin->y = min(vmin->y, v.y); + vmin->z = min(vmin->z, v.z); +} +void SMesh::GetBounding(Vector *vmax, Vector *vmin) const { + int i; + *vmin = Vector::From( 1e12, 1e12, 1e12); + *vmax = Vector::From(-1e12, -1e12, -1e12); + for(i = 0; i < l.n; i++) { + const STriangle *st = &(l[i]); + DoBounding(st->a, vmax, vmin); + DoBounding(st->b, vmax, vmin); + DoBounding(st->c, vmax, vmin); + } +} + +//---------------------------------------------------------------------------- +// Report the edges of the boundary of the region(s) of our mesh that lie +// within the plane n dot p = d. +//---------------------------------------------------------------------------- +void SMesh::MakeEdgesInPlaneInto(SEdgeList *sel, Vector n, double d) { + SMesh m = {}; + m.MakeFromCopyOf(this); + + // Delete all triangles in the mesh that do not lie in our export plane. + m.l.ClearTags(); + int i; + for(i = 0; i < m.l.n; i++) { + STriangle *tr = &(m.l[i]); + + if((fabs(n.Dot(tr->a) - d) >= LENGTH_EPS) || + (fabs(n.Dot(tr->b) - d) >= LENGTH_EPS) || + (fabs(n.Dot(tr->c) - d) >= LENGTH_EPS)) + { + tr->tag = 1; + } + } + m.l.RemoveTagged(); + + // Select the naked edges in our resulting open mesh. + SKdNode *root = SKdNode::From(&m); + root->SnapToMesh(&m); + root->MakeCertainEdgesInto(sel, EdgeKind::NAKED_OR_SELF_INTER, + /*coplanarIsInter=*/false, NULL, NULL); + + m.Clear(); +} + +void SMesh::MakeOutlinesInto(SOutlineList *sol, EdgeKind edgeKind) { + SKdNode *root = SKdNode::From(this); + root->MakeOutlinesInto(sol, edgeKind); +} + +//----------------------------------------------------------------------------- +// When we are called, all of the triangles from l[start] to the end must +// be coplanar. So we try to find a set of fewer triangles that covers the +// exact same area, in order to reduce the number of triangles in the mesh. +// We use this after a triangle has been split against the BSP. +// +// This is really ugly code; basically it just pastes things together to +// form convex polygons, merging collinear edges when possible, then +// triangulates the convex poly. +//----------------------------------------------------------------------------- +void SMesh::Simplify(int start) { + int maxTriangles = (l.n - start) + 10; + + STriMeta meta = l[start].meta; + + STriangle *tout = new STriangle[maxTriangles]; + int toutc = 0; + + Vector n = Vector::From(0, 0, 0); + Vector *conv = new Vector[maxTriangles * 3]; + int convc = 0; + + int start0 = start; + + int i, j; + for(i = start; i < l.n; i++) { + STriangle *tr = &(l[i]); + if(tr->MinAltitude() < LENGTH_EPS) { + tr->tag = 1; + } else { + tr->tag = 0; + } + } + + for(;;) { + bool didAdd; + convc = 0; + for(i = start; i < l.n; i++) { + STriangle *tr = &(l[i]); + if(tr->tag) continue; + + tr->tag = 1; + n = (tr->Normal()).WithMagnitude(1); + conv[convc++] = tr->a; + conv[convc++] = tr->b; + conv[convc++] = tr->c; + + start = i+1; + break; + } + if(i >= l.n) break; + + do { + didAdd = false; + + for(j = 0; j < convc; j++) { + Vector a = conv[WRAP((j-1), convc)], + b = conv[j], + d = conv[WRAP((j+1), convc)], + e = conv[WRAP((j+2), convc)]; + + Vector c; + for(i = start; i < l.n; i++) { + STriangle *tr = &(l[i]); + if(tr->tag) continue; + + if((tr->a).Equals(d) && (tr->b).Equals(b)) { + c = tr->c; + } else if((tr->b).Equals(d) && (tr->c).Equals(b)) { + c = tr->a; + } else if((tr->c).Equals(d) && (tr->a).Equals(b)) { + c = tr->b; + } else { + continue; + } + // The vertex at C must be convex; but the others must + // be tested + Vector ab = b.Minus(a); + Vector bc = c.Minus(b); + Vector cd = d.Minus(c); + Vector de = e.Minus(d); + + double bDot = (ab.Cross(bc)).Dot(n); + double dDot = (cd.Cross(de)).Dot(n); + + bDot /= min(ab.Magnitude(), bc.Magnitude()); + dDot /= min(cd.Magnitude(), de.Magnitude()); + + if(fabs(bDot) < LENGTH_EPS && fabs(dDot) < LENGTH_EPS) { + conv[WRAP((j+1), convc)] = c; + // and remove the vertex at j, which is a dup + std::move(conv+j+1, conv+convc, conv+j); + convc--; + } else if(fabs(bDot) < LENGTH_EPS && dDot > 0) { + conv[j] = c; + } else if(fabs(dDot) < LENGTH_EPS && bDot > 0) { + conv[WRAP((j+1), convc)] = c; + } else if(bDot > 0 && dDot > 0) { + // conv[j] is unchanged, conv[j+1] goes to [j+2] + std::move_backward(conv+j+1, conv+convc, conv+convc+1); + conv[j+1] = c; + convc++; + } else { + continue; + } + + didAdd = true; + tr->tag = 1; + break; + } + } + } while(didAdd); + + // I need to debug why this is required; sometimes the above code + // still generates a convex polygon + for(i = 0; i < convc; i++) { + Vector a = conv[WRAP((i-1), convc)], + b = conv[i], + c = conv[WRAP((i+1), convc)]; + Vector ab = b.Minus(a); + Vector bc = c.Minus(b); + double bDot = (ab.Cross(bc)).Dot(n); + bDot /= min(ab.Magnitude(), bc.Magnitude()); + + if(bDot < 0) return; // XXX, shouldn't happen + } + + for(i = 0; i < convc - 2; i++) { + STriangle tr = STriangle::From(meta, conv[0], conv[i+1], conv[i+2]); + if(tr.MinAltitude() > LENGTH_EPS) { + tout[toutc++] = tr; + } + } + } + + l.n = start0; + for(i = 0; i < toutc; i++) { + AddTriangle(&(tout[i])); + } + delete[] tout; + delete[] conv; +} + +void SMesh::AddAgainstBsp(SMesh *srcm, SBsp3 *bsp3) { + int i; + + for(i = 0; i < srcm->l.n; i++) { + STriangle *st = &(srcm->l[i]); + int pn = l.n; + atLeastOneDiscarded = false; + SBsp3::InsertOrCreate(bsp3, st, this); + if(!atLeastOneDiscarded && (l.n != (pn+1))) { + l.n = pn; + if(flipNormal) { + AddTriangle(st->meta, st->c, st->b, st->a); + } else { + AddTriangle(st->meta, st->a, st->b, st->c); + } + } + if(l.n - pn > 1) { + Simplify(pn); + } + } +} + +void SMesh::MakeFromUnionOf(SMesh *a, SMesh *b) { + SBsp3 *bspa = SBsp3::FromMesh(a); + SBsp3 *bspb = SBsp3::FromMesh(b); + + flipNormal = false; + keepInsideOtherShell = false; + + keepCoplanar = true; + AddAgainstBsp(b, bspa); + + keepCoplanar = false; + AddAgainstBsp(a, bspb); +} + +void SMesh::MakeFromDifferenceOf(SMesh *a, SMesh *b) { + SBsp3 *bspa = SBsp3::FromMesh(a); + SBsp3 *bspb = SBsp3::FromMesh(b); + + flipNormal = true; + keepCoplanar = true; + keepInsideOtherShell = true; + AddAgainstBsp(b, bspa); + + flipNormal = false; + keepCoplanar = false; + keepInsideOtherShell = false; + AddAgainstBsp(a, bspb); +} + +void SMesh::MakeFromIntersectionOf(SMesh *a, SMesh *b) { + SBsp3 *bspa = SBsp3::FromMesh(a); + SBsp3 *bspb = SBsp3::FromMesh(b); + + keepInsideOtherShell = true; + flipNormal = false; + + keepCoplanar = false; + AddAgainstBsp(a, bspb); + + keepCoplanar = true; + AddAgainstBsp(b, bspa); +} + +void SMesh::MakeFromCopyOf(SMesh *a) { + ssassert(this != a, "Can't make from copy of self"); + for(int i = 0; i < a->l.n; i++) { + AddTriangle(&(a->l[i])); + } +} + +void SMesh::MakeFromAssemblyOf(SMesh *a, SMesh *b) { + MakeFromCopyOf(a); + MakeFromCopyOf(b); +} + +void SMesh::MakeFromTransformationOf(SMesh *a, Vector trans, + Quaternion q, double scale) +{ + STriangle *tr; + for(tr = a->l.First(); tr; tr = a->l.NextAfter(tr)) { + STriangle tt = *tr; + tt.a = (tt.a).ScaledBy(scale); + tt.b = (tt.b).ScaledBy(scale); + tt.c = (tt.c).ScaledBy(scale); + if(scale < 0) { + // The mirroring would otherwise turn a closed mesh inside out. + swap(tt.a, tt.b); + } + tt.a = (q.Rotate(tt.a)).Plus(trans); + tt.b = (q.Rotate(tt.b)).Plus(trans); + tt.c = (q.Rotate(tt.c)).Plus(trans); + AddTriangle(&tt); + } +} + +bool SMesh::IsEmpty() const { return (l.IsEmpty()); } + +uint32_t SMesh::FirstIntersectionWith(Point2d mp) const { + Vector rayPoint = SS.GW.UnProjectPoint3(Vector::From(mp.x, mp.y, 0.0)); + Vector rayDir = SS.GW.UnProjectPoint3(Vector::From(mp.x, mp.y, 1.0)).Minus(rayPoint); + + uint32_t face = 0; + double faceT = VERY_NEGATIVE; + for(int i = 0; i < l.n; i++) { + const STriangle &tr = l[i]; + if(tr.meta.face == 0) continue; + + double t; + if(!tr.Raytrace(rayPoint, rayDir, &t, NULL)) continue; + if(t > faceT) { + face = tr.meta.face; + faceT = t; + } + } + + return face; +} + +Vector SMesh::GetCenterOfMass() const { + Vector center = {}; + double vol = 0.0; + for(int i = 0; i < l.n; i++) { + const STriangle &tr = l[i]; + double tvol = tr.SignedVolume(); + center = center.Plus(tr.a.Plus(tr.b.Plus(tr.c)).ScaledBy(tvol / 4.0)); + vol += tvol; + } + return center.ScaledBy(1.0 / vol); +} + +STriangleLl *STriangleLl::Alloc() + { return (STriangleLl *)Platform::AllocTemporary(sizeof(STriangleLl)); } +SKdNode *SKdNode::Alloc() + { return (SKdNode *)Platform::AllocTemporary(sizeof(SKdNode)); } + +SKdNode *SKdNode::From(SMesh *m) { + int i; + STriangle *tra = (STriangle *)Platform::AllocTemporary((m->l.n) * sizeof(*tra)); + + for(i = 0; i < m->l.n; i++) { + tra[i] = m->l[i]; + } + + srand(0); + int n = m->l.n; + while(n > 1) { + int k = rand() % n; + n--; + swap(tra[k], tra[n]); + } + + STriangleLl *tll = NULL; + for(i = 0; i < m->l.n; i++) { + STriangleLl *tn = STriangleLl::Alloc(); + tn->tri = &(tra[i]); + tn->next = tll; + tll = tn; + } + + return SKdNode::From(tll); +} + +SKdNode *SKdNode::From(STriangleLl *tll) { + int which = 0; + SKdNode *ret = Alloc(); + + int i; + int gtc[3] = { 0, 0, 0 }, ltc[3] = { 0, 0, 0 }, allc = 0; + double badness[3] = { 0, 0, 0 }; + double split[3] = { 0, 0, 0 }; + + if(!tll) { + goto leaf; + } + + for(i = 0; i < 3; i++) { + int tcnt = 0; + STriangleLl *ll; + for(ll = tll; ll; ll = ll->next) { + split[i] += (ll->tri->a).Element(i); + split[i] += (ll->tri->b).Element(i); + split[i] += (ll->tri->c).Element(i); + tcnt++; + } + split[i] /= (tcnt*3); + + for(ll = tll; ll; ll = ll->next) { + STriangle *tr = ll->tri; + + double a = (tr->a).Element(i), + b = (tr->b).Element(i), + c = (tr->c).Element(i); + + if(a < split[i] + KDTREE_EPS || + b < split[i] + KDTREE_EPS || + c < split[i] + KDTREE_EPS) + { + ltc[i]++; + } + if(a > split[i] - KDTREE_EPS || + b > split[i] - KDTREE_EPS || + c > split[i] - KDTREE_EPS) + { + gtc[i]++; + } + if(i == 0) allc++; + } + badness[i] = pow((double)ltc[i], 4) + pow((double)gtc[i], 4); + } + if(badness[0] < badness[1] && badness[0] < badness[2]) { + which = 0; + } else if(badness[1] < badness[2]) { + which = 1; + } else { + which = 2; + } + + if(allc < 3 || allc == gtc[which] || allc == ltc[which]) { + goto leaf; + } + + STriangleLl *ll; + STriangleLl *lgt, *llt; lgt = llt = NULL; + for(ll = tll; ll; ll = ll->next) { + STriangle *tr = ll->tri; + + double a = (tr->a).Element(which), + b = (tr->b).Element(which), + c = (tr->c).Element(which); + + if(a < split[which] + KDTREE_EPS || + b < split[which] + KDTREE_EPS || + c < split[which] + KDTREE_EPS) + { + STriangleLl *n = STriangleLl::Alloc(); + *n = *ll; + n->next = llt; + llt = n; + } + if(a > split[which] - KDTREE_EPS || + b > split[which] - KDTREE_EPS || + c > split[which] - KDTREE_EPS) + { + STriangleLl *n = STriangleLl::Alloc(); + *n = *ll; + n->next = lgt; + lgt = n; + } + } + + ret->which = which; + ret->c = split[which]; + ret->gt = SKdNode::From(lgt); + ret->lt = SKdNode::From(llt); + return ret; + +leaf: + ret->tris = tll; + return ret; +} + +void SKdNode::ClearTags() const { + if(gt && lt) { + gt->ClearTags(); + lt->ClearTags(); + } else { + STriangleLl *ll; + for(ll = tris; ll; ll = ll->next) { + ll->tri->tag = 0; + } + } +} + +void SKdNode::AddTriangle(STriangle *tr) { + if(gt && lt) { + double ta = (tr->a).Element(which), + tb = (tr->b).Element(which), + tc = (tr->c).Element(which); + if(ta < c + KDTREE_EPS || + tb < c + KDTREE_EPS || + tc < c + KDTREE_EPS) + { + lt->AddTriangle(tr); + } + if(ta > c - KDTREE_EPS || + tb > c - KDTREE_EPS || + tc > c - KDTREE_EPS) + { + gt->AddTriangle(tr); + } + } else { + STriangleLl *tn = STriangleLl::Alloc(); + tn->tri = tr; + tn->next = tris; + tris = tn; + } +} + +void SKdNode::MakeMeshInto(SMesh *m) const { + if(gt) gt->MakeMeshInto(m); + if(lt) lt->MakeMeshInto(m); + + STriangleLl *ll; + for(ll = tris; ll; ll = ll->next) { + if(ll->tri->tag) continue; + + m->AddTriangle(ll->tri); + ll->tri->tag = 1; + } +} + +void SKdNode::ListTrianglesInto(std::vector *tl) const { + if(gt) gt->ListTrianglesInto(tl); + if(lt) lt->ListTrianglesInto(tl); + + STriangleLl *ll; + for(ll = tris; ll; ll = ll->next) { + if(ll->tri->tag) continue; + + tl->push_back(ll->tri); + ll->tri->tag = 1; + } +} + +//----------------------------------------------------------------------------- +// If any triangles in the mesh have an edge that goes through v (but not +// a vertex at v), then split those triangles so that they now have a vertex +// there. The existing triangle is modified, and the new triangle appears +// in extras. +//----------------------------------------------------------------------------- +void SKdNode::SnapToVertex(Vector v, SMesh *extras) { + if(gt && lt) { + double vc = v.Element(which); + if(vc < c + KDTREE_EPS) { + lt->SnapToVertex(v, extras); + } + if(vc > c - KDTREE_EPS) { + gt->SnapToVertex(v, extras); + } + // Nothing bad happens if the triangle to be split appears in both + // branches; the first call will split the triangle, so that the + // second call will do nothing, because the modified triangle will + // already contain v + } else { + STriangleLl *ll; + for(ll = tris; ll; ll = ll->next) { + STriangle *tr = ll->tri; + + // Do a cheap bbox test first + int k; + bool mightHit = true; + + for(k = 0; k < 3; k++) { + double trA = (tr->a).Element(k); + double trB = (tr->b).Element(k); + double trC = (tr->c).Element(k); + double vk = v.Element(k); + if(trA < vk - KDTREE_EPS && + trB < vk - KDTREE_EPS && + trC < vk - KDTREE_EPS) + { + mightHit = false; + break; + } + if(trA > vk + KDTREE_EPS && + trB > vk + KDTREE_EPS && + trC > vk + KDTREE_EPS) + { + mightHit = false; + break; + } + } + if(!mightHit) continue; + + if(tr->a.Equals(v)) { tr->a = v; continue; } + if(tr->b.Equals(v)) { tr->b = v; continue; } + if(tr->c.Equals(v)) { tr->c = v; continue; } + + if(tr->IsDegenerate()) { + continue; + } + + if(v.OnLineSegment(tr->a, tr->b)) { + STriangle nt = STriangle::From(tr->meta, tr->a, v, tr->c); + extras->AddTriangle(&nt); + tr->a = v; + continue; + } + if(v.OnLineSegment(tr->b, tr->c)) { + STriangle nt = STriangle::From(tr->meta, tr->b, v, tr->a); + extras->AddTriangle(&nt); + tr->b = v; + continue; + } + if(v.OnLineSegment(tr->c, tr->a)) { + STriangle nt = STriangle::From(tr->meta, tr->c, v, tr->b); + extras->AddTriangle(&nt); + tr->c = v; + continue; + } + } + } +} + +//----------------------------------------------------------------------------- +// Snap to each vertex of each triangle of the given mesh. If the given mesh +// is identical to the mesh used to make this kd tree, then the result should +// be a vertex-to-vertex mesh. +//----------------------------------------------------------------------------- +void SKdNode::SnapToMesh(SMesh *m) { + int i, j, k; + for(i = 0; i < m->l.n; i++) { + STriangle *tr = &(m->l[i]); + if(tr->IsDegenerate()) { + continue; + } + for(j = 0; j < 3; j++) { + Vector v = tr->vertices[j]; + + SMesh extra = {}; + SnapToVertex(v, &extra); + + for(k = 0; k < extra.l.n; k++) { + STriangle *tra = (STriangle *)Platform::AllocTemporary(sizeof(*tra)); + *tra = extra.l[k]; + AddTriangle(tra); + } + extra.Clear(); + } + } +} + +//----------------------------------------------------------------------------- +// For all the edges in sel, split them against the given triangle, and test +// them for occlusion. sel is both our input and our output. tag indicates +// whether an edge is occluded. +//----------------------------------------------------------------------------- +void SKdNode::SplitLinesAgainstTriangle(SEdgeList *sel, STriangle *tr) const { + SEdgeList seln = {}; + + Vector tn = tr->Normal().WithMagnitude(1); + double td = tn.Dot(tr->a); + + // Consider front-facing triangles only. + if(tn.z > LENGTH_EPS) { + // If the edge crosses our triangle's plane, then split into above + // and below parts. Note that we must preserve auxA, which contains + // the style associated with this line, as well as the tag, which + // contains the occlusion status. + SEdge *se; + for(se = sel->l.First(); se; se = sel->l.NextAfter(se)) { + double da = (se->a).Dot(tn) - td, + db = (se->b).Dot(tn) - td; + if((da < -LENGTH_EPS && db > LENGTH_EPS) || + (db < -LENGTH_EPS && da > LENGTH_EPS)) + { + Vector m = Vector::AtIntersectionOfPlaneAndLine( + tn, td, + se->a, se->b, NULL); + seln.AddEdge(m, se->b, se->auxA, 0, se->tag); + se->b = m; + } + } + for(se = seln.l.First(); se; se = seln.l.NextAfter(se)) { + sel->AddEdge(se->a, se->b, se->auxA, 0, se->tag); + } + seln.Clear(); + + for(se = sel->l.First(); se; se = sel->l.NextAfter(se)) { + Vector pt = ((se->a).Plus(se->b)).ScaledBy(0.5); + if(pt.Dot(tn) - td > -LENGTH_EPS) { + // Edge is in front of or on our plane (remember, tn.z > 0) + // so it is exempt from further splitting + se->auxB = 1; + } else { + // Edge is behind our plane, needs further splitting + se->auxB = 0; + } + } + + // Considering only the (x, y) coordinates, split the edge against our + // triangle. + Point2d a = (tr->a).ProjectXy(), + b = (tr->b).ProjectXy(), + c = (tr->c).ProjectXy(); + + Point2d n[3] = { (b.Minus(a)).Normal().WithMagnitude(1), + (c.Minus(b)).Normal().WithMagnitude(1), + (a.Minus(c)).Normal().WithMagnitude(1) }; + + double d[3] = { n[0].Dot(b), + n[1].Dot(c), + n[2].Dot(a) }; + + // Split all of the edges where they intersect the triangle edges + int i; + for(i = 0; i < 3; i++) { + for(se = sel->l.First(); se; se = sel->l.NextAfter(se)) { + if(se->auxB) continue; + + Point2d ap = (se->a).ProjectXy(), + bp = (se->b).ProjectXy(); + double da = n[i].Dot(ap) - d[i], + db = n[i].Dot(bp) - d[i]; + if((da < -LENGTH_EPS && db > LENGTH_EPS) || + (db < -LENGTH_EPS && da > LENGTH_EPS)) + { + double dab = (db - da); + Vector spl = ((se->a).ScaledBy( db/dab)).Plus( + (se->b).ScaledBy(-da/dab)); + seln.AddEdge(spl, se->b, se->auxA, 0, se->tag); + se->b = spl; + } + } + for(se = seln.l.First(); se; se = seln.l.NextAfter(se)) { + // The split pieces are all behind the triangle, since only + // edges behind the triangle got split. So their auxB is 0. + sel->AddEdge(se->a, se->b, se->auxA, 0, se->tag); + } + seln.Clear(); + } + + for(se = sel->l.First(); se; se = sel->l.NextAfter(se)) { + bool occluded; + if(se->auxB) { + // Lies above or on the triangle plane, so triangle doesn't + // occlude it. + occluded = false; + } else { + // Test the segment to see if it lies outside the triangle + // (i.e., outside wrt at least one edge), and keep it only + // then. + Point2d pt = ((se->a).Plus(se->b).ScaledBy(0.5)).ProjectXy(); + occluded = true; + for(i = 0; i < 3; i++) { + // If the test point lies on the boundary of our triangle, + // then we still discard the edge. + if(n[i].Dot(pt) - d[i] > LENGTH_EPS) occluded = false; + } + } + + if(occluded) { + se->tag = 1; + } + } + } +} + +//----------------------------------------------------------------------------- +// Given an edge orig, occlusion test it against our mesh. We output an edge +// list in sel, where only invisible portions of the edge are tagged. +//----------------------------------------------------------------------------- +void SKdNode::OcclusionTestLine(SEdge orig, SEdgeList *sel, int cnt) const { + if(gt && lt) { + double ac = (orig.a).Element(which), + bc = (orig.b).Element(which); + // We can ignore triangles that are separated in x or y, but triangles + // that are separated in z may still contribute + if(ac < c + KDTREE_EPS || + bc < c + KDTREE_EPS || + which == 2) + { + lt->OcclusionTestLine(orig, sel, cnt); + } + if(ac > c - KDTREE_EPS || + bc > c - KDTREE_EPS || + which == 2) + { + gt->OcclusionTestLine(orig, sel, cnt); + } + } else { + STriangleLl *ll; + for(ll = tris; ll; ll = ll->next) { + STriangle *tr = ll->tri; + + if(tr->tag == cnt) continue; + + SplitLinesAgainstTriangle(sel, tr); + tr->tag = cnt; + } + } +} + +//----------------------------------------------------------------------------- +// Search the mesh for a triangle with an edge from b to a (i.e., the mate +// for the edge from a to b), and increment info->count each time that we +// find one. If a triangle is found, then report whether it is front- or +// back-facing using info->frontFacing. And regardless of whether a mate is +// found, report whether the edge intersects the mesh with info->intersectsMesh; +// if coplanarIsInter then we count the edge as intersecting if it's coplanar +// with a triangle in the mesh, otherwise not. +//----------------------------------------------------------------------------- +void SKdNode::FindEdgeOn(Vector a, Vector b, int cnt, bool coplanarIsInter, + EdgeOnInfo *info) const +{ + if(gt && lt) { + double ac = a.Element(which), + bc = b.Element(which); + if(ac < c + KDTREE_EPS || + bc < c + KDTREE_EPS) + { + lt->FindEdgeOn(a, b, cnt, coplanarIsInter, info); + } + if(ac > c - KDTREE_EPS || + bc > c - KDTREE_EPS) + { + gt->FindEdgeOn(a, b, cnt, coplanarIsInter, info); + } + return; + } + + // We are a leaf node; so we iterate over all the triangles in our + // linked list. + STriangleLl *ll; + for(ll = tris; ll; ll = ll->next) { + STriangle *tr = ll->tri; + + if(tr->tag == cnt) continue; + + // Test if this triangle matches up with the given edge + if((a.Equals(tr->b) && b.Equals(tr->a)) || + (a.Equals(tr->c) && b.Equals(tr->b)) || + (a.Equals(tr->a) && b.Equals(tr->c))) + { + info->count++; + // Record whether this triangle is front- or back-facing. + if(tr->Normal().z > LENGTH_EPS) { + info->frontFacing = true; + } else { + info->frontFacing = false; + } + // Record the triangle + info->tr = tr; + // And record which vertices a and b correspond to + info->ai = a.Equals(tr->a) ? 0 : (a.Equals(tr->b) ? 1 : 2); + info->bi = b.Equals(tr->a) ? 0 : (b.Equals(tr->b) ? 1 : 2); + } else if(((a.Equals(tr->a) && b.Equals(tr->b)) || + (a.Equals(tr->b) && b.Equals(tr->c)) || + (a.Equals(tr->c) && b.Equals(tr->a)))) + { + // It's an edge of this triangle, okay. + } else { + // Check for self-intersection + Vector n = (tr->Normal()).WithMagnitude(1); + double d = (tr->a).Dot(n); + double pa = a.Dot(n) - d, pb = b.Dot(n) - d; + // It's an intersection if neither point lies in-plane, + // and the edge crosses the plane (should handle in-plane + // intersections separately but don't yet). + if((pa < -LENGTH_EPS || pa > LENGTH_EPS) && + (pb < -LENGTH_EPS || pb > LENGTH_EPS) && + (pa*pb < 0)) + { + // The edge crosses the plane of the triangle; now see if + // it crosses inside the triangle. + if(tr->ContainsPointProjd(b.Minus(a), a)) { + if(coplanarIsInter) { + info->intersectsMesh = true; + } else { + Vector p = Vector::AtIntersectionOfPlaneAndLine( + n, d, a, b, NULL); + Vector ta = tr->a, + tb = tr->b, + tc = tr->c; + if((p.DistanceToLine(ta, tb.Minus(ta)) < LENGTH_EPS) || + (p.DistanceToLine(tb, tc.Minus(tb)) < LENGTH_EPS) || + (p.DistanceToLine(tc, ta.Minus(tc)) < LENGTH_EPS)) + { + // Intersection lies on edge. This happens when + // our edge is from a triangle coplanar with + // another triangle in the mesh. We don't test + // the edge against triangles whose plane contains + // that edge, but we do end up testing against + // the coplanar triangle's neighbours, which we + // will intersect on their edges. + } else { + info->intersectsMesh = true; + } + } + } + } + } + + // Ensure that we don't count this triangle twice if it appears + // in two buckets of the kd tree. + tr->tag = cnt; + } +} + +static bool CheckAndAddTrianglePair(std::set> *pairs, + STriangle *a, STriangle *b) +{ + if(pairs->find(std::make_pair(a, b)) != pairs->end() || + pairs->find(std::make_pair(b, a)) != pairs->end()) + return true; + + pairs->emplace(a, b); + return false; +} + +//----------------------------------------------------------------------------- +// Pick certain classes of edges out from our mesh. These might be: +// * naked edges (i.e., edges with no anti-parallel neighbor) and self- +// intersecting edges (i.e., edges that cross another triangle) +// * turning edges (i.e., edges where a front-facing triangle joins +// a back-facing triangle) +// * emphasized edges (i.e., edges where a triangle from one face joins +// a triangle from a different face) +//----------------------------------------------------------------------------- +void SKdNode::MakeCertainEdgesInto(SEdgeList *sel, EdgeKind how, bool coplanarIsInter, + bool *inter, bool *leaky, int auxA) const +{ + if(inter) *inter = false; + if(leaky) *leaky = false; + + std::vector tris; + ClearTags(); + ListTrianglesInto(&tris); + + std::set> edgeTris; + int cnt = 1234; + for(STriangle *tr : tris) { + for(int j = 0; j < 3; j++) { + Vector a = tr->vertices[j]; + Vector b = tr->vertices[(j + 1) % 3]; + + SKdNode::EdgeOnInfo info = {}; + FindEdgeOn(a, b, cnt, coplanarIsInter, &info); + + switch(how) { + case EdgeKind::NAKED_OR_SELF_INTER: + // there should be one anti-parllel edge + if(info.count != 1) { + // but there may be multiple parallel coincident edges + SKdNode::EdgeOnInfo parallelInfo = {}; + FindEdgeOn(b, a, -cnt, coplanarIsInter, ¶llelInfo); + if (info.count != parallelInfo.count) { + sel->AddEdge(a, b, auxA); + if(leaky) *leaky = true; + } + } + if(info.intersectsMesh) { + sel->AddEdge(a, b, auxA); + if(inter) *inter = true; + } + break; + + case EdgeKind::SELF_INTER: + if(info.intersectsMesh) { + sel->AddEdge(a, b, auxA); + if(inter) *inter = true; + } + break; + + case EdgeKind::TURNING: + if((tr->Normal().z < LENGTH_EPS) && + (info.count == 1) && + info.frontFacing) + { + if(CheckAndAddTrianglePair(&edgeTris, tr, info.tr)) + break; + // This triangle is back-facing (or on edge), and + // this edge has exactly one mate, and that mate is + // front-facing. So this is a turning edge. + sel->AddEdge(a, b, auxA); + } + break; + + case EdgeKind::EMPHASIZED: + if(info.count == 1 && tr->meta.face != info.tr->meta.face) { + if(CheckAndAddTrianglePair(&edgeTris, tr, info.tr)) + break; + // The two triangles that join at this edge come from + // different faces; either really different faces, + // or one is from a face and the other is zero (i.e., + // not from a face). + sel->AddEdge(a, b, auxA); + } + break; + + case EdgeKind::SHARP: + if(info.count == 1) { + Vector na0 = tr->normals[j].WithMagnitude(1.0); + Vector nb0 = tr->normals[(j + 1) % 3].WithMagnitude(1.0); + Vector na1 = info.tr->normals[info.ai].WithMagnitude(1.0); + Vector nb1 = info.tr->normals[info.bi].WithMagnitude(1.0); + if(!((na0.Equals(na1) && nb0.Equals(nb1)) || + (na0.Equals(nb1) && nb0.Equals(na1)))) { + if(CheckAndAddTrianglePair(&edgeTris, tr, info.tr)) + break; + // The two triangles that join at this edge meet at a sharp + // angle. This implies they come from different faces. + sel->AddEdge(a, b, auxA); + } + } + break; + } + + cnt++; + } + } +} + +void SKdNode::MakeOutlinesInto(SOutlineList *sol, EdgeKind edgeKind) const +{ + std::vector tris; + ClearTags(); + ListTrianglesInto(&tris); + + std::set> edgeTris; + int cnt = 1234; + for(STriangle *tr : tris) { + for(int j = 0; j < 3; j++) { + Vector a = tr->vertices[j]; + Vector b = tr->vertices[(j + 1) % 3]; + + SKdNode::EdgeOnInfo info = {}; + FindEdgeOn(a, b, cnt, /*coplanarIsInter=*/false, &info); + cnt++; + if(info.count != 1) continue; + if(CheckAndAddTrianglePair(&edgeTris, tr, info.tr)) + continue; + + int tag = 0; + switch(edgeKind) { + case EdgeKind::EMPHASIZED: + if(tr->meta.face != info.tr->meta.face) { + tag = 1; + } + break; + + case EdgeKind::SHARP: { + Vector na0 = tr->normals[j].WithMagnitude(1.0); + Vector nb0 = tr->normals[(j + 1) % 3].WithMagnitude(1.0); + Vector na1 = info.tr->normals[info.ai].WithMagnitude(1.0); + Vector nb1 = info.tr->normals[info.bi].WithMagnitude(1.0); + if(!((na0.Equals(na1) && nb0.Equals(nb1)) || + (na0.Equals(nb1) && nb0.Equals(na1)))) { + tag = 1; + } + } + break; + + default: + ssassert(false, "Unexpected edge kind"); + } + + Vector nl = tr->Normal().WithMagnitude(1.0); + Vector nr = info.tr->Normal().WithMagnitude(1.0); + + // We don't add edges with the same left and right + // normals because they can't produce outlines. + if(tag == 0 && nl.Equals(nr)) continue; + sol->AddEdge(a, b, nl, nr, tag); + } + } +} + +bool SOutline::IsVisible(Vector projDir) const { + double ldot = nl.Dot(projDir); + double rdot = nr.Dot(projDir); + return (ldot > -LENGTH_EPS) == (rdot < LENGTH_EPS) || + (rdot > -LENGTH_EPS) == (ldot < LENGTH_EPS); +} + +void SOutlineList::Clear() { + l.Clear(); +} + +void SOutlineList::AddEdge(Vector a, Vector b, Vector nl, Vector nr, int tag) { + SOutline so = {}; + so.a = a; + so.b = b; + so.nl = nl; + so.nr = nr; + so.tag = tag; + l.Add(&so); +} + +void SOutlineList::ListTaggedInto(SEdgeList *el, int auxA, int auxB) { + for(const SOutline &so : l) { + if(so.tag == 0) continue; + el->AddEdge(so.a, so.b, auxA, auxB); + } +} + +void SOutlineList::MakeFromCopyOf(SOutlineList *sol) { + for(SOutline *so = sol->l.First(); so; so = sol->l.NextAfter(so)) { + l.Add(so); + } +} + +void SMesh::PrecomputeTransparency() { + std::sort(l.begin(), l.end(), + [&](const STriangle &sta, const STriangle &stb) { + RgbaColor colora = sta.meta.color, + colorb = stb.meta.color; + bool opaquea = colora.IsEmpty() || colora.alpha == 255, + opaqueb = colorb.IsEmpty() || colorb.alpha == 255; + + if(!opaquea || !opaqueb) isTransparent = true; + return (opaquea != opaqueb && opaquea == true); + }); +} + +void SMesh::RemoveDegenerateTriangles() { + for(auto &tr : l) { + tr.tag = (int)tr.IsDegenerate(); + } + l.RemoveTagged(); +} + +double SMesh::CalculateVolume() const { + double vol = 0; + for(STriangle tr : l) { + // Translate to place vertex A at (x, y, 0) + Vector trans = Vector::From(tr.a.x, tr.a.y, 0); + tr.a = (tr.a).Minus(trans); + tr.b = (tr.b).Minus(trans); + tr.c = (tr.c).Minus(trans); + + // Rotate to place vertex B on the y-axis. Depending on + // whether the triangle is CW or CCW, C is either to the + // right or to the left of the y-axis. This handles the + // sign of our normal. + Vector u = Vector::From(-tr.b.y, tr.b.x, 0); + u = u.WithMagnitude(1); + Vector v = Vector::From(tr.b.x, tr.b.y, 0); + v = v.WithMagnitude(1); + Vector n = Vector::From(0, 0, 1); + + tr.a = (tr.a).DotInToCsys(u, v, n); + tr.b = (tr.b).DotInToCsys(u, v, n); + tr.c = (tr.c).DotInToCsys(u, v, n); + + n = tr.Normal().WithMagnitude(1); + + // Triangles on edge don't contribute + if(fabs(n.z) < LENGTH_EPS) continue; + + // The plane has equation p dot n = a dot n + double d = (tr.a).Dot(n); + // nx*x + ny*y + nz*z = d + // nz*z = d - nx*x - ny*y + double A = -n.x/n.z, B = -n.y/n.z, C = d/n.z; + + double mac = tr.c.y/tr.c.x, mbc = (tr.c.y - tr.b.y)/tr.c.x; + double xc = tr.c.x, yb = tr.b.y; + + // I asked Maple for + // int(int(A*x + B*y +C, y=mac*x..(mbc*x + yb)), x=0..xc); + double integral = + (1.0/3)*( + A*(mbc-mac)+ + (1.0/2)*B*(mbc*mbc-mac*mac) + )*(xc*xc*xc)+ + (1.0/2)*(A*yb+B*yb*mbc+C*(mbc-mac))*xc*xc+ + C*yb*xc+ + (1.0/2)*B*yb*yb*xc; + + vol += integral; + } + return vol; +} + +double SMesh::CalculateSurfaceArea(const std::vector &faces) const { + double area = 0.0; + for(uint32_t f : faces) { + for(const STriangle &t : l) { + if(f != t.meta.face) continue; + area += t.Area(); + } + } + return area; +} + +} // namespace SolveSpace diff --git a/src/modify.cpp b/src/modify.cpp new file mode 100644 index 0000000..60079df --- /dev/null +++ b/src/modify.cpp @@ -0,0 +1,751 @@ +//----------------------------------------------------------------------------- +// User-initiated (not parametric) operations to modify our sketch, by +// changing the requests, like to round a corner or split curves where they +// intersect. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +//----------------------------------------------------------------------------- +// Replace constraints on oldpt with the same constraints on newpt. +// Useful when splitting, tangent arcing, or removing bezier points. +//----------------------------------------------------------------------------- +void GraphicsWindow::ReplacePointInConstraints(hEntity oldpt, hEntity newpt) { + for(auto &c : SK.constraint) { + if(c.ptA == oldpt) + c.ptA = newpt; + if(c.ptB == oldpt) + c.ptB = newpt; + } +} + +//----------------------------------------------------------------------------- +// Remove constraints on hpt. Useful when removing bezier points. +//----------------------------------------------------------------------------- +void GraphicsWindow::RemoveConstraintsForPointBeingDeleted(hEntity hpt) { + SK.constraint.ClearTags(); + for(auto &c : SK.constraint) { + if(c.ptA == hpt || c.ptB == hpt) { + c.tag = 1; + (SS.deleted.constraints)++; + if(c.type != Constraint::Type::POINTS_COINCIDENT && + c.type != Constraint::Type::HORIZONTAL && + c.type != Constraint::Type::VERTICAL) + { + (SS.deleted.nonTrivialConstraints)++; + } + } + } + SK.constraint.RemoveTagged(); +} + +//----------------------------------------------------------------------------- +// Let's say that A is coincident with B, and B is coincident with C. This +// implies that A is coincident with C; but if we delete B, then both +// constraints must be deleted too (since they reference B), and A is no +// longer constrained to C. This routine adds back that constraint. +//----------------------------------------------------------------------------- +void GraphicsWindow::FixConstraintsForRequestBeingDeleted(hRequest hr) { + Request *r = SK.GetRequest(hr); + if(r->group != SS.GW.activeGroup) return; + + for(Entity &e : SK.entity) { + if(!(e.h.isFromRequest())) continue; + if(e.h.request() != hr) continue; + + if(e.type != Entity::Type::POINT_IN_2D && + e.type != Entity::Type::POINT_IN_3D) + { + continue; + } + + // This is a point generated by the request being deleted; so fix + // the constraints for that. + FixConstraintsForPointBeingDeleted(e.h); + } +} +void GraphicsWindow::FixConstraintsForPointBeingDeleted(hEntity hpt) { + List ld = {}; + + SK.constraint.ClearTags(); + for(Constraint &c : SK.constraint) { + if(c.type != Constraint::Type::POINTS_COINCIDENT) continue; + if(c.group != SS.GW.activeGroup) continue; + + if(c.ptA == hpt) { + ld.Add(&(c.ptB)); + c.tag = 1; + } + if(c.ptB == hpt) { + ld.Add(&(c.ptA)); + c.tag = 1; + } + } + // Remove constraints without waiting for regeneration; this way + // if another point takes the place of the deleted one (e.g. when + // removing control points of a bezier) the constraint doesn't + // spuriously move. Similarly, subsequent calls of this function + // (if multiple coincident points are getting deleted) will work + // correctly. + SK.constraint.RemoveTagged(); + + // If more than one point was constrained coincident with hpt, then + // those two points were implicitly coincident with each other. By + // deleting hpt (and all constraints that mention it), we will delete + // that relationship. So put it back here now. + for(int i = 1; i < ld.n; i++) { + Constraint::ConstrainCoincident(ld[i-1], ld[i]); + } + ld.Clear(); +} + +//----------------------------------------------------------------------------- +// A curve by its parametric equation, helper functions for computing tangent +// arcs by a numerical method. +//----------------------------------------------------------------------------- +void GraphicsWindow::ParametricCurve::MakeFromEntity(hEntity he, bool reverse) { + *this = {}; + Entity *e = SK.GetEntity(he); + if(e->type == Entity::Type::LINE_SEGMENT) { + isLine = true; + p0 = e->EndpointStart(), + p1 = e->EndpointFinish(); + if(reverse) { + swap(p0, p1); + } + } else if(e->type == Entity::Type::ARC_OF_CIRCLE) { + isLine = false; + p0 = SK.GetEntity(e->point[0])->PointGetNum(); + Vector pe = SK.GetEntity(e->point[1])->PointGetNum(); + r = (pe.Minus(p0)).Magnitude(); + e->ArcGetAngles(&theta0, &theta1, &dtheta); + if(reverse) { + swap(theta0, theta1); + dtheta = -dtheta; + } + EntityBase *wrkpln = SK.GetEntity(e->workplane)->Normal(); + u = wrkpln->NormalU(); + v = wrkpln->NormalV(); + } else ssassert(false, "Unexpected entity type"); +} +double GraphicsWindow::ParametricCurve::LengthForAuto() { + if(isLine) { + // Allow a third of the line to disappear with auto radius + return (p1.Minus(p0)).Magnitude() / 3; + } else { + // But only a twentieth of the arc; shorter means fewer numerical + // problems since the curve is more linear over shorter sections. + return (fabs(dtheta)*r)/20; + } +} +Vector GraphicsWindow::ParametricCurve::PointAt(double t) { + if(isLine) { + return p0.Plus((p1.Minus(p0)).ScaledBy(t)); + } else { + double theta = theta0 + dtheta*t; + return p0.Plus(u.ScaledBy(r*cos(theta)).Plus(v.ScaledBy(r*sin(theta)))); + } +} +Vector GraphicsWindow::ParametricCurve::TangentAt(double t) { + if(isLine) { + return p1.Minus(p0); + } else { + double theta = theta0 + dtheta*t; + Vector t = u.ScaledBy(-r*sin(theta)).Plus(v.ScaledBy(r*cos(theta))); + t = t.ScaledBy(dtheta); + return t; + } +} +/** Changes or copies the given entity and connects it to the arc. + * \param t Where on this parametric curve does it connect to the arc. + * \param reuseOrig Should the original entity be modified + * \param orig The original entity. + * \param arc The arc that will be connected to. + * \param arcFinish Whether to connect to the end point of the arc. + * \param pointf When changing the original entity, whether the end point should be modified. + */ +void GraphicsWindow::ParametricCurve::CreateRequestTrimmedTo(double t, + bool reuseOrig, hEntity orig, hEntity arc, bool arcFinish, bool pointf) +{ + hRequest hr; + Entity *e; + if(isLine) { + if (reuseOrig) { + e = SK.GetEntity(orig); + int i = pointf ? 1 : 0; + SK.GetEntity(e->point[i])->PointForceTo(PointAt(t)); + ConstrainPointIfCoincident(e->point[i]); + } else { + hr = SS.GW.AddRequest(Request::Type::LINE_SEGMENT, /*rememberForUndo=*/false), + e = SK.GetEntity(hr.entity(0)); + SK.GetEntity(e->point[0])->PointForceTo(PointAt(t)); + SK.GetEntity(e->point[1])->PointForceTo(PointAt(1)); + ConstrainPointIfCoincident(e->point[0]); + ConstrainPointIfCoincident(e->point[1]); + Constraint::Constrain(Constraint::Type::PT_ON_LINE, + hr.entity(1), Entity::NO_ENTITY, orig); + } + Constraint::Constrain(Constraint::Type::ARC_LINE_TANGENT, + Entity::NO_ENTITY, Entity::NO_ENTITY, + arc, e->h, /*other=*/arcFinish, /*other2=*/false); + } else { + if (reuseOrig) { + e = SK.GetEntity(orig); + int i = pointf ? 2 : 1; + SK.GetEntity(e->point[i])->PointForceTo(PointAt(t)); + ConstrainPointIfCoincident(e->point[i]); + } else { + hr = SS.GW.AddRequest(Request::Type::ARC_OF_CIRCLE, /*rememberForUndo=*/false), + e = SK.GetEntity(hr.entity(0)); + SK.GetEntity(e->point[0])->PointForceTo(p0); + if(dtheta > 0) { + SK.GetEntity(e->point[1])->PointForceTo(PointAt(t)); + SK.GetEntity(e->point[2])->PointForceTo(PointAt(1)); + } else { + SK.GetEntity(e->point[2])->PointForceTo(PointAt(t)); + SK.GetEntity(e->point[1])->PointForceTo(PointAt(1)); + } + ConstrainPointIfCoincident(e->point[0]); + ConstrainPointIfCoincident(e->point[1]); + ConstrainPointIfCoincident(e->point[2]); + } + // The tangency constraint alone is enough to fully constrain it, + // so there's no need for more. + Constraint::Constrain(Constraint::Type::CURVE_CURVE_TANGENT, + Entity::NO_ENTITY, Entity::NO_ENTITY, + arc, e->h, /*other=*/arcFinish, /*other2=*/(dtheta < 0)); + } +} + +//----------------------------------------------------------------------------- +// If a point in the same group as hpt, and numerically coincident with hpt, +// happens to exist, then constrain that point coincident to hpt. +//----------------------------------------------------------------------------- +void GraphicsWindow::ParametricCurve::ConstrainPointIfCoincident(hEntity hpt) { + Entity *pt; + pt = SK.GetEntity(hpt); + Vector ev, ptv; + ptv = pt->PointGetNum(); + + for(Entity &e : SK.entity) { + if(e.h == pt->h) continue; + if(!e.IsPoint()) continue; + if(e.group != pt->group) continue; + if(e.workplane != pt->workplane) continue; + + ev = e.PointGetNum(); + if(!ev.Equals(ptv)) continue; + + Constraint::ConstrainCoincident(hpt, e.h); + break; + } +} + +//----------------------------------------------------------------------------- +// A single point must be selected when this function is called. We find two +// non-construction line segments that join at this point, and create a +// tangent arc joining them. +//----------------------------------------------------------------------------- +void GraphicsWindow::MakeTangentArc() { + if(!LockedInWorkplane()) { + Error(_("Must be sketching in workplane to create tangent arc.")); + return; + } + + // The point corresponding to the vertex to be rounded. + Vector pshared = SK.GetEntity(gs.point[0])->PointGetNum(); + ClearSelection(); + + // First, find two requests (that are not construction, and that are + // in our group and workplane) that generate entities that have an + // endpoint at our vertex to be rounded. + int i, c = 0; + Entity *ent[2]; + Request *req[2]; + hRequest hreq[2]; + hEntity hent[2]; + bool pointf[2]; + for(auto &r : SK.request) { + if(r.group != activeGroup) + continue; + if(r.workplane != ActiveWorkplane()) + continue; + if(r.construction) + continue; + if(r.type != Request::Type::LINE_SEGMENT && r.type != Request::Type::ARC_OF_CIRCLE) { + continue; + } + + Entity *e = SK.GetEntity(r.h.entity(0)); + Vector ps = e->EndpointStart(), + pf = e->EndpointFinish(); + + if(ps.Equals(pshared) || pf.Equals(pshared)) { + if(c < 2) { + // We record the entity and request and their handles, + // and whether the vertex to be rounded is the start or + // finish of this entity. + ent[c] = e; + hent[c] = e->h; + req[c] = &r; + hreq[c] = r.h; + pointf[c] = (pf.Equals(pshared)); + } + c++; + } + } + if(c != 2) { + Error(_("To create a tangent arc, select a point where two " + "non-construction lines or circles in this group and " + "workplane join.")); + return; + } + + Entity *wrkpl = SK.GetEntity(ActiveWorkplane()); + Vector wn = wrkpl->Normal()->NormalN(); + + // Based on these two entities, we make the objects that we'll use to + // numerically find the tangent arc. + ParametricCurve pc[2]; + pc[0].MakeFromEntity(ent[0]->h, pointf[0]); + pc[1].MakeFromEntity(ent[1]->h, pointf[1]); + + // And thereafter we mustn't touch the entity or req ptrs, + // because the new requests/entities we add might force a + // realloc. + memset(ent, 0, sizeof(ent)); + memset(req, 0, sizeof(req)); + + Vector pinter; + double r = 0.0, vv = 0.0; + // We now do Newton iterations to find the tangent arc, and its positions + // t back along the two curves, starting from shared point of the curves + // at t = 0. Lots of iterations helps convergence, and this is still + // ~10 ms for everything. + int iters = 1000; + double t[2] = { 0, 0 }, tp[2]; + for(i = 0; i < iters + 20; i++) { + Vector p0 = pc[0].PointAt(t[0]), + p1 = pc[1].PointAt(t[1]), + t0 = pc[0].TangentAt(t[0]), + t1 = pc[1].TangentAt(t[1]); + + pinter = Vector::AtIntersectionOfLines(p0, p0.Plus(t0), + p1, p1.Plus(t1), + NULL, NULL, NULL); + + // The sign of vv determines whether shortest distance is + // clockwise or anti-clockwise. + Vector v = (wn.Cross(t0)).WithMagnitude(1); + vv = t1.Dot(v); + + double dot = (t0.WithMagnitude(1)).Dot(t1.WithMagnitude(1)); + double theta = acos(dot); + + if(SS.tangentArcManual) { + r = SS.tangentArcRadius; + } else { + r = 200/scale; + // Set the radius so that no more than one third of the + // line segment disappears. + r = min(r, pc[0].LengthForAuto()*tan(theta/2)); + r = min(r, pc[1].LengthForAuto()*tan(theta/2));; + } + // We are source-stepping the radius, to improve convergence. So + // ramp that for most of the iterations, and then do a few at + // the end with that constant for polishing. + if(i < iters) { + r *= 0.1 + 0.9*i/((double)iters); + } + + // The distance from the intersection of the lines to the endpoint + // of the arc, along each line. + double el = r/tan(theta/2); + + // Compute the endpoints of the arc, for each curve + Vector pa0 = pinter.Plus(t0.WithMagnitude(el)), + pa1 = pinter.Plus(t1.WithMagnitude(el)); + + tp[0] = t[0]; + tp[1] = t[1]; + + // And convert those points to parameter values along the curve. + t[0] += (pa0.Minus(p0)).DivProjected(t0); + t[1] += (pa1.Minus(p1)).DivProjected(t1); + } + + // Stupid check for convergence, and for an out of range result (as + // we would get, for example, if the line is too short to fit the + // rounding arc). + if(fabs(tp[0] - t[0]) > 1e-3 || fabs(tp[1] - t[1]) > 1e-3 || + t[0] < 0.01 || t[1] < 0.01 || + t[0] > 0.99 || t[1] > 0.99 || + IsReasonable(t[0]) || IsReasonable(t[1])) + { + Error(_("Couldn't round this corner. Try a smaller radius, or try " + "creating the desired geometry by hand with tangency " + "constraints.")); + return; + } + + // Compute the location of the center of the arc + Vector center = pc[0].PointAt(t[0]), + v0inter = pinter.Minus(center); + int a, b; + if(vv < 0) { + a = 1; b = 2; + center = center.Minus(v0inter.Cross(wn).WithMagnitude(r)); + } else { + a = 2; b = 1; + center = center.Plus(v0inter.Cross(wn).WithMagnitude(r)); + } + + SS.UndoRemember(); + + if (SS.tangentArcModify) { + // Delete the coincident constraint for the removed point. + SK.constraint.ClearTags(); + for(i = 0; i < SK.constraint.n; i++) { + Constraint *cs = &(SK.constraint[i]); + if(cs->group != activeGroup) continue; + if(cs->workplane != ActiveWorkplane()) continue; + if(cs->type != Constraint::Type::POINTS_COINCIDENT) continue; + if (SK.GetEntity(cs->ptA)->PointGetNum().Equals(pshared)) { + cs->tag = 1; + } + } + SK.constraint.RemoveTagged(); + } else { + // Make the original entities construction, or delete them + // entirely, according to user preference. + SK.GetRequest(hreq[0])->construction = true; + SK.GetRequest(hreq[1])->construction = true; + } + + // Create and position the new tangent arc. + hRequest harc = AddRequest(Request::Type::ARC_OF_CIRCLE, /*rememberForUndo=*/false); + Entity *earc = SK.GetEntity(harc.entity(0)); + hEntity hearc = earc->h; + + SK.GetEntity(earc->point[0])->PointForceTo(center); + SK.GetEntity(earc->point[a])->PointForceTo(pc[0].PointAt(t[0])); + SK.GetEntity(earc->point[b])->PointForceTo(pc[1].PointAt(t[1])); + + earc = NULL; + + // Modify or duplicate the original entities and connect them to the tangent arc. + pc[0].CreateRequestTrimmedTo(t[0], SS.tangentArcModify, + hent[0], hearc, /*arcFinish=*/(b == 1), pointf[0]); + pc[1].CreateRequestTrimmedTo(t[1], SS.tangentArcModify, + hent[1], hearc, /*arcFinish=*/(a == 1), pointf[1]); +} + +hEntity GraphicsWindow::SplitLine(hEntity he, Vector pinter) { + // Save the original endpoints, since we're about to delete this entity. + Entity *e01 = SK.GetEntity(he); + hEntity hep0 = e01->point[0], hep1 = e01->point[1]; + Vector p0 = SK.GetEntity(hep0)->PointGetNum(), + p1 = SK.GetEntity(hep1)->PointGetNum(); + + if(p0.Equals(pinter)) { + return hep0; + } + + if(p1.Equals(pinter)) { + return hep1; + } + + // Add the two line segments this one gets split into. + hRequest r0i = AddRequest(Request::Type::LINE_SEGMENT, /*rememberForUndo=*/false), + ri1 = AddRequest(Request::Type::LINE_SEGMENT, /*rememberForUndo=*/false); + // Don't get entities till after adding, realloc issues + + Entity *e0i = SK.GetEntity(r0i.entity(0)), + *ei1 = SK.GetEntity(ri1.entity(0)); + + SK.GetEntity(e0i->point[0])->PointForceTo(p0); + SK.GetEntity(e0i->point[1])->PointForceTo(pinter); + SK.GetEntity(ei1->point[0])->PointForceTo(pinter); + SK.GetEntity(ei1->point[1])->PointForceTo(p1); + + ReplacePointInConstraints(hep0, e0i->point[0]); + ReplacePointInConstraints(hep1, ei1->point[1]); + Constraint::ConstrainCoincident(e0i->point[1], ei1->point[0]); + return e0i->point[1]; +} + +hEntity GraphicsWindow::SplitCircle(hEntity he, Vector pinter) { + Entity *circle = SK.GetEntity(he); + if(circle->type == Entity::Type::CIRCLE) { + // Start with an unbroken circle, split it into a 360 degree arc. + Vector center = SK.GetEntity(circle->point[0])->PointGetNum(); + + circle = NULL; // shortly invalid! + hRequest hr = AddRequest(Request::Type::ARC_OF_CIRCLE, /*rememberForUndo=*/false); + + Entity *arc = SK.GetEntity(hr.entity(0)); + + SK.GetEntity(arc->point[0])->PointForceTo(center); + SK.GetEntity(arc->point[1])->PointForceTo(pinter); + SK.GetEntity(arc->point[2])->PointForceTo(pinter); + + Constraint::ConstrainCoincident(arc->point[1], arc->point[2]); + return arc->point[1]; + } else { + // Start with an arc, break it in to two arcs + hEntity hc = circle->point[0], + hs = circle->point[1], + hf = circle->point[2]; + Vector center = SK.GetEntity(hc)->PointGetNum(), + start = SK.GetEntity(hs)->PointGetNum(), + finish = SK.GetEntity(hf)->PointGetNum(); + + circle = NULL; // shortly invalid! + hRequest hr0 = AddRequest(Request::Type::ARC_OF_CIRCLE, /*rememberForUndo=*/false), + hr1 = AddRequest(Request::Type::ARC_OF_CIRCLE, /*rememberForUndo=*/false); + + Entity *arc0 = SK.GetEntity(hr0.entity(0)), + *arc1 = SK.GetEntity(hr1.entity(0)); + + SK.GetEntity(arc0->point[0])->PointForceTo(center); + SK.GetEntity(arc0->point[1])->PointForceTo(start); + SK.GetEntity(arc0->point[2])->PointForceTo(pinter); + + SK.GetEntity(arc1->point[0])->PointForceTo(center); + SK.GetEntity(arc1->point[1])->PointForceTo(pinter); + SK.GetEntity(arc1->point[2])->PointForceTo(finish); + + ReplacePointInConstraints(hs, arc0->point[1]); + ReplacePointInConstraints(hf, arc1->point[2]); + Constraint::ConstrainCoincident(arc0->point[2], arc1->point[1]); + return arc0->point[2]; + } +} + +hEntity GraphicsWindow::SplitCubic(hEntity he, Vector pinter) { + // Save the original endpoints, since we're about to delete this entity. + Entity *e01 = SK.GetEntity(he); + SBezierList sbl = {}; + e01->GenerateBezierCurves(&sbl); + + hEntity hep0 = e01->point[0], + hep1 = e01->point[3+e01->extraPoints], + hep0n = Entity::NO_ENTITY, // the new start point + hep1n = Entity::NO_ENTITY, // the new finish point + hepin = Entity::NO_ENTITY; // the intersection point + + // The curve may consist of multiple cubic segments. So find which one + // contains the intersection point. + double t; + int i, j; + for(i = 0; i < sbl.l.n; i++) { + SBezier *sb = &(sbl.l[i]); + ssassert(sb->deg == 3, "Expected a cubic bezier"); + + sb->ClosestPointTo(pinter, &t, /*mustConverge=*/false); + if(pinter.Equals(sb->PointAt(t))) { + // Split that segment at the intersection. + SBezier b0i, bi1, b01 = *sb; + b01.SplitAt(t, &b0i, &bi1); + + // Add the two cubic segments this one gets split into. + hRequest r0i = AddRequest(Request::Type::CUBIC, /*rememberForUndo=*/false), + ri1 = AddRequest(Request::Type::CUBIC, /*rememberForUndo=*/false); + // Don't get entities till after adding, realloc issues + + Entity *e0i = SK.GetEntity(r0i.entity(0)), + *ei1 = SK.GetEntity(ri1.entity(0)); + + for(j = 0; j <= 3; j++) { + SK.GetEntity(e0i->point[j])->PointForceTo(b0i.ctrl[j]); + } + for(j = 0; j <= 3; j++) { + SK.GetEntity(ei1->point[j])->PointForceTo(bi1.ctrl[j]); + } + + Constraint::ConstrainCoincident(e0i->point[3], ei1->point[0]); + if(i == 0) hep0n = e0i->point[0]; + hep1n = ei1->point[3]; + hepin = e0i->point[3]; + } else { + hRequest r = AddRequest(Request::Type::CUBIC, /*rememberForUndo=*/false); + Entity *e = SK.GetEntity(r.entity(0)); + + for(j = 0; j <= 3; j++) { + SK.GetEntity(e->point[j])->PointForceTo(sb->ctrl[j]); + } + + if(i == 0) hep0n = e->point[0]; + hep1n = e->point[3]; + } + } + + sbl.Clear(); + + ReplacePointInConstraints(hep0, hep0n); + ReplacePointInConstraints(hep1, hep1n); + return hepin; +} + +hEntity GraphicsWindow::SplitEntity(hEntity he, Vector pinter) { + Entity *e = SK.GetEntity(he); + + hEntity ret; + if(e->IsCircle()) { + ret = SplitCircle(he, pinter); + } else if(e->type == Entity::Type::LINE_SEGMENT) { + ret = SplitLine(he, pinter); + } else if(e->type == Entity::Type::CUBIC || e->type == Entity::Type::CUBIC_PERIODIC) { + ret = SplitCubic(he, pinter); + } else { + Error(_("Couldn't split this entity; lines, circles, or cubics only.")); + return Entity::NO_ENTITY; + } + + // Finally, delete the request that generated the original entity. + if(he.isFromRequest() && he == he.request().entity(0)) { + hRequest hr = he.request(); + // Only delete the original request if we actually made a split + // (i.e. the split point is not from the original request) + if(hr != ret.request()) { + Request *r = SK.GetRequest(hr); + // If the user wants to keep the old entities around, they can just + // mark them construction first. + if(r->group == activeGroup && !r->construction) { + r->tag = 1; + DeleteTaggedRequests(); + } + } + } + + return ret; +} + +void GraphicsWindow::SplitLinesOrCurves() { + if(!LockedInWorkplane()) { + Error(_("Must be sketching in workplane to split.")); + return; + } + + GroupSelection(); + int n = gs.lineSegments + gs.circlesOrArcs + gs.cubics + gs.periodicCubics; + if(!((n == 2 && gs.points == 0) || (n == 1 && gs.points == 1))) { + Error(_("Select two entities that intersect each other " + "(e.g. two lines/circles/arcs or a line/circle/arc and a point).")); + return; + } + + bool splitAtPoint = (gs.points == 1); + hEntity ha = gs.entity[0], + hb = splitAtPoint ? gs.point[0] : gs.entity[1]; + + Entity *ea = SK.GetEntity(ha), + *eb = SK.GetEntity(hb); + Vector pi = Vector::From(0, 0, 0); + + // First, decide the point where we're going to make the split. + bool foundInters = false; + if(splitAtPoint) { + // One of the entities is a point, and this point must be on the other entity. + // Verify that a corresponding point-coincident constraint exists for the point/entity. + Vector p0, p1; + if(ea->type == Entity::Type::LINE_SEGMENT) { + p0 = ea->EndpointStart(); + p1 = ea->EndpointFinish(); + } + + SK.constraint.ClearTags(); + + for(Constraint &c : SK.constraint) { + if(c.ptA.request() == hb.request() && + c.entityA.request() == ha.request()) { + pi = SK.GetEntity(c.ptA)->PointGetNum(); + + if(ea->type == Entity::Type::LINE_SEGMENT && !pi.OnLineSegment(p0, p1)) { + // The point isn't between line endpoints, so there isn't an actual + // intersection. + continue; + } + + c.tag = 1; + foundInters = true; + break; + } + } + } else { + // Compute the possibly-rational Bezier curves for each of these non-point entities... + SBezierList sbla = {}, sblb = {}; + ea->GenerateBezierCurves(&sbla); + eb->GenerateBezierCurves(&sblb); + // ... and then compute the points where they intersect, based on those curves. + SPointList inters = {}; + sbla.AllIntersectionsWith(&sblb, &inters); + + // If there's multiple points, then take the one closest to the mouse pointer. + if(!inters.l.IsEmpty()) { + double dmin = VERY_POSITIVE; + for(const SPoint &sp : inters.l) { + double d = ProjectPoint(sp.p).DistanceTo(currentMousePosition); + if(d < dmin) { + dmin = d; + pi = sp.p; + } + } + + foundInters = true; + } + + inters.Clear(); + sbla.Clear(); + sblb.Clear(); + } + + // Then, actually split the entities. + if(foundInters) { + SS.UndoRemember(); + + // Remove any constraints we're going to replace. + SK.constraint.RemoveTagged(); + + hEntity hia = SplitEntity(ha, pi), + hib = {}; + // SplitEntity adds the coincident constraints to join the split halves + // of each original entity; and then we add the constraint to join + // the two entities together at the split point. + if(splitAtPoint) { + // Remove datum point, as it has now been superseded by the split point. + SK.request.ClearTags(); + for(Request &r : SK.request) { + if(r.h == hb.request()) { + if(r.type == Request::Type::DATUM_POINT) { + // Delete datum point. + r.tag = 1; + FixConstraintsForRequestBeingDeleted(r.h); + } else { + // Add constraint if not datum point, but endpoint of line/arc etc. + Constraint::ConstrainCoincident(hia, hb); + } + break; + } + } + SK.request.RemoveTagged(); + } else { + // Split second non-point entity and add constraint. + hib = SplitEntity(hb, pi); + if(hia.v && hib.v) { + Constraint::ConstrainCoincident(hia, hib); + } + } + } else { + Error(_("Can't split; no intersection found.")); + return; + } + + // All done, clean up and regenerate. + ClearSelection(); +} + +} // namespace SolveSpace diff --git a/src/mouse.cpp b/src/mouse.cpp new file mode 100644 index 0000000..6aa0c58 --- /dev/null +++ b/src/mouse.cpp @@ -0,0 +1,1599 @@ +//----------------------------------------------------------------------------- +// Anything relating to mouse, keyboard, or 6-DOF mouse input. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +void GraphicsWindow::UpdateDraggedPoint(hEntity hp, double mx, double my) { + Entity *p = SK.GetEntity(hp); + Vector pos = p->PointGetNum(); + UpdateDraggedNum(&pos, mx, my); + p->PointForceTo(pos); + + SS.ScheduleShowTW(); +} + +void GraphicsWindow::UpdateDraggedNum(Vector *pos, double mx, double my) { + *pos = pos->Plus(projRight.ScaledBy((mx - orig.mouse.x)/scale)); + *pos = pos->Plus(projUp.ScaledBy((my - orig.mouse.y)/scale)); +} + +void GraphicsWindow::AddPointToDraggedList(hEntity hp) { + Entity *p = SK.GetEntity(hp); + // If an entity and its points are both selected, then its points could + // end up in the list twice. This would be bad, because it would move + // twice as far as the mouse pointer... + List *lhe = &(pending.points); + for(hEntity *hee = lhe->First(); hee; hee = lhe->NextAfter(hee)) { + if(*hee == hp) { + // Exact same point. + return; + } + Entity *pe = SK.GetEntity(*hee); + if(pe->type == p->type && + pe->type != Entity::Type::POINT_IN_2D && + pe->type != Entity::Type::POINT_IN_3D && + pe->group == p->group) + { + // Transform-type point, from the same group. So it handles the + // same unknowns. + return; + } + } + pending.points.Add(&hp); +} + +void GraphicsWindow::StartDraggingByEntity(hEntity he) { + Entity *e = SK.GetEntity(he); + if(e->IsPoint()) { + AddPointToDraggedList(e->h); + } else if(e->type == Entity::Type::LINE_SEGMENT || + e->type == Entity::Type::ARC_OF_CIRCLE || + e->type == Entity::Type::CUBIC || + e->type == Entity::Type::CUBIC_PERIODIC || + e->type == Entity::Type::CIRCLE || + e->type == Entity::Type::TTF_TEXT || + e->type == Entity::Type::IMAGE) + { + int pts; + ssassert(EntReqTable::GetEntityInfo(e->type, e->extraPoints, + NULL, &pts, NULL, NULL), "No entity info"); + for(int i = 0; i < pts; i++) { + AddPointToDraggedList(e->point[i]); + } + } else if(e->type == Entity::Type::FACE_NORMAL_PT + || e->type == Entity::Type::FACE_N_TRANS // this is for translate groups + + // rotate groups are currently unstable dragging +// || e->type == Entity::Type::FACE_N_ROT_AA + +// Revolve is unstable because the point is on axis. Helix drags in axial direction for same reason +// || e->type == Entity::Type::FACE_ROT_NORMAL_PT ) { // needed for helix and revolve + + || e->type == Entity::Type::FACE_N_ROT_TRANS) { // needed for linked objects + // Files save prior to v3.2 didn't specify point[0] + // so check existence before using it. + Entity *pt = SK.entity.FindByIdNoOops(e->point[0]); + if(pt) + { + // for some reason Revolve top/bottom faces do bad things if they can't be dragged. + // but a two sided revolve group will behave OK. + if(pt->CanBeDragged()) { + AddPointToDraggedList(e->point[0]); + } + } + } +} + +void GraphicsWindow::StartDraggingBySelection() { + List *ls = &(selection); + for(Selection *s = ls->First(); s; s = ls->NextAfter(s)) { + if(!s->entity.v) continue; + + StartDraggingByEntity(s->entity); + } + // The user might select a point, and then click it again to start + // dragging; but the point just got unselected by that click. So drag + // the hovered item too, and they'll always have it. + if(hover.entity.v) { + hEntity dragEntity = ChooseFromHoverToDrag().entity; + if(dragEntity != Entity::NO_ENTITY) { + StartDraggingByEntity(dragEntity); + } + } +} + +void GraphicsWindow::MouseMoved(double x, double y, bool leftDown, + bool middleDown, bool rightDown, bool shiftDown, bool ctrlDown) +{ + if(window->IsEditorVisible()) return; + if(context.active) return; + + SS.extraLine.draw = false; + + if(!orig.mouseDown) { + // If someone drags the mouse into our window with the left button + // already depressed, then we don't have our starting point; so + // don't try. + leftDown = false; + } + + if(rightDown) { + middleDown = true; + shiftDown = !shiftDown; + } + + // Not passing right-button and middle-button drags to the toolbar avoids + // some cosmetic issues with trackpad pans/rotates implemented with + // simulated right-button drag events causing spurious hover events. + if(SS.showToolbar && !middleDown) { + if(ToolbarMouseMoved((int)x, (int)y)) { + hover.Clear(); + return; + } + } + + if(!leftDown && (pending.operation == Pending::DRAGGING_POINTS || + pending.operation == Pending::DRAGGING_MARQUEE)) + { + ClearPending(); + Invalidate(); + } + + Point2d mp = Point2d::From(x, y); + currentMousePosition = mp; + + if(rightDown && orig.mouse.DistanceTo(mp) < 5 && !orig.startedMoving) { + // Avoid accidentally panning (or rotating if shift is down) if the + // user wants a context menu. + return; + } + orig.startedMoving = true; + + // If the middle button is down, then mouse movement is used to pan and + // rotate our view. This wins over everything else. + if(middleDown) { + hover.Clear(); + + double dx = (x - orig.mouse.x) / scale; + double dy = (y - orig.mouse.y) / scale; + + if(!(shiftDown || ctrlDown)) { + double sign = SS.cameraNav ? -1.0 : 1.0; + double s = 0.3*(PI/180)*scale*sign; // degrees per pixel + if(SS.turntableNav) { // lock the Z to vertical + projRight = orig.projRight.RotatedAbout(Vector::From(0, 0, 1), -s * dx); + projUp = orig.projUp.RotatedAbout( + Vector::From(orig.projRight.x, orig.projRight.y, orig.projRight.y), s * dy); + } else { + projRight = orig.projRight.RotatedAbout(orig.projUp, -s * dx); + projUp = orig.projUp.RotatedAbout(orig.projRight, s * dy); + } + + NormalizeProjectionVectors(); + } else if(ctrlDown) { + double theta = atan2(orig.mouse.y, orig.mouse.x); + theta -= atan2(y, x); + SS.extraLine.draw = true; + SS.extraLine.ptA = UnProjectPoint(Point2d::From(0, 0)); + SS.extraLine.ptB = UnProjectPoint(mp); + + Vector normal = orig.projRight.Cross(orig.projUp); + projRight = orig.projRight.RotatedAbout(normal, theta); + projUp = orig.projUp.RotatedAbout(normal, theta); + + NormalizeProjectionVectors(); + } else { + offset.x = orig.offset.x + dx*projRight.x + dy*projUp.x; + offset.y = orig.offset.y + dx*projRight.y + dy*projUp.y; + offset.z = orig.offset.z + dx*projRight.z + dy*projUp.z; + } + + orig.projRight = projRight; + orig.projUp = projUp; + orig.offset = offset; + orig.mouse.x = x; + orig.mouse.y = y; + + if(SS.TW.shown.screen == TextWindow::Screen::EDIT_VIEW) { + if(havePainted) { + SS.ScheduleShowTW(); + } + } + Invalidate(); + havePainted = false; + return; + } + + if(pending.operation == Pending::NONE) { + double dm = orig.mouse.DistanceTo(mp); + // If we're currently not doing anything, then see if we should + // start dragging something. + if(leftDown && dm > 3) { + Entity *e = NULL; + hEntity dragEntity = ChooseFromHoverToDrag().entity; + if(dragEntity.v) e = SK.GetEntity(dragEntity); + if(e && e->type != Entity::Type::WORKPLANE) { + if(!hoverWasSelectedOnMousedown) { + // The user clicked an unselected entity, which + // means they're dragging just the hovered thing, + // not the full selection. So clear all the selection + // except that entity. + ClearSelection(); + MakeSelected(dragEntity); + } + if(e->type == Entity::Type::CIRCLE && selection.n <= 1) { + // Drag the radius. + pending.circle = dragEntity; + pending.operation = Pending::DRAGGING_RADIUS; + } else if(e->IsNormal()) { + pending.normal = dragEntity; + pending.operation = Pending::DRAGGING_NORMAL; + } else { + StartDraggingBySelection(); + hover.Clear(); + pending.operation = Pending::DRAGGING_POINTS; + } + } else if(hover.constraint.v && + SK.GetConstraint(hover.constraint)->HasLabel()) + { + ClearSelection(); + pending.constraint = hover.constraint; + pending.operation = Pending::DRAGGING_CONSTRAINT; + } + if(pending.operation != Pending::NONE) { + // We just started a drag, so remember for the undo before + // the drag changes anything. + SS.UndoRemember(); + } else { + if(!hover.constraint.v) { + // That's just marquee selection, which should not cause + // an undo remember. + if(dm > 10) { + if(hover.entity.v) { + // Avoid accidentally selecting workplanes when + // starting drags. + MakeUnselected(hover.entity, /*coincidentPointTrick=*/false); + hover.Clear(); + } + pending.operation = Pending::DRAGGING_MARQUEE; + orig.marqueePoint = + UnProjectPoint(orig.mouseOnButtonDown); + } + } + } + } else { + // Otherwise, just hit test and give up; but don't hit test + // if the mouse is down, because then the user could hover + // a point, mouse down (thus selecting it), and drag, in an + // effort to drag the point, but instead hover a different + // entity before we move far enough to start the drag. + if(!leftDown) { + // Hit testing can potentially take a lot of time. + // If we haven't painted since last time we highlighted + // something, don't hit test again, since this just causes + // a lag. + if(!havePainted) return; + HitTestMakeSelection(mp); + } + } + return; + } + + // If the user has started an operation from the menu, but not + // completed it, then just do the selection. + if(pending.operation == Pending::COMMAND) { + HitTestMakeSelection(mp); + return; + } + + if(pending.operation == Pending::DRAGGING_POINTS && ctrlDown) { + SS.extraLine.ptA = UnProjectPoint(orig.mouseOnButtonDown); + SS.extraLine.ptB = UnProjectPoint(mp); + SS.extraLine.draw = true; + } + + // We're currently dragging something; so do that. But if we haven't + // painted since the last time we solved, do nothing, because there's + // no sense solving a frame and not displaying it. + if(!havePainted) { + return; + } + + havePainted = false; + switch(pending.operation) { + case Pending::DRAGGING_CONSTRAINT: { + Constraint *c = SK.constraint.FindById(pending.constraint); + UpdateDraggedNum(&(c->disp.offset), x, y); + orig.mouse = mp; + Invalidate(); + return; + } + + case Pending::DRAGGING_NEW_LINE_POINT: + if(!ctrlDown) { + SS.GW.pending.hasSuggestion = + SS.GW.SuggestLineConstraint(SS.GW.pending.request, &SS.GW.pending.suggestion); + } else { + SS.GW.pending.hasSuggestion = false; + } + // fallthrough + case Pending::DRAGGING_NEW_POINT: + UpdateDraggedPoint(pending.point, x, y); + HitTestMakeSelection(mp); + SS.MarkGroupDirtyByEntity(pending.point); + orig.mouse = mp; + break; + + case Pending::DRAGGING_POINTS: + if(shiftDown || ctrlDown) { + // Edit the rotation associated with a POINT_N_ROT_TRANS, + // either within (ctrlDown) or out of (shiftDown) the plane + // of the screen. So first get the rotation to apply, in qt. + Quaternion qt; + if(ctrlDown) { + double d = mp.DistanceTo(orig.mouseOnButtonDown); + if(d < 25) { + // Don't start dragging the position about the normal + // until we're a little ways out, to get a reasonable + // reference pos + qt = Quaternion::IDENTITY; + } else { + double theta = atan2(orig.mouse.y-orig.mouseOnButtonDown.y, + orig.mouse.x-orig.mouseOnButtonDown.x); + theta -= atan2(y-orig.mouseOnButtonDown.y, + x-orig.mouseOnButtonDown.x); + + Vector gn = projRight.Cross(projUp); + qt = Quaternion::From(gn, -theta); + } + } else { + double dx = -(x - orig.mouse.x); + double dy = -(y - orig.mouse.y); + double s = 0.3*(PI/180); // degrees per pixel + qt = Quaternion::From(projUp, -s*dx).Times( + Quaternion::From(projRight, s*dy)); + } + + // Now apply this rotation to the points being dragged. + List *lhe = &(pending.points); + for(hEntity *he = lhe->First(); he; he = lhe->NextAfter(he)) { + Entity *e = SK.GetEntity(*he); + if(e->type != Entity::Type::POINT_N_ROT_TRANS) { + if(ctrlDown) { + Vector p = e->PointGetNum(); + p = p.Minus(SS.extraLine.ptA); + p = qt.Rotate(p); + p = p.Plus(SS.extraLine.ptA); + e->PointForceTo(p); + } else { + UpdateDraggedPoint(*he, x, y); + } + } else { + Quaternion q = e->PointGetQuaternion(); + Vector p = e->PointGetNum(); + q = qt.Times(q); + e->PointForceQuaternionTo(q); + // Let's rotate about the selected point; so fix up the + // translation so that that point didn't move. + e->PointForceTo(p); + } + SS.MarkGroupDirtyByEntity(e->h); + } + } else { + List *lhe = &(pending.points); + for(hEntity *he = lhe->First(); he; he = lhe->NextAfter(he)) { + UpdateDraggedPoint(*he, x, y); + SS.MarkGroupDirtyByEntity(*he); + } + } + orig.mouse = mp; + break; + + case Pending::DRAGGING_NEW_CUBIC_POINT: { + UpdateDraggedPoint(pending.point, x, y); + HitTestMakeSelection(mp); + + hRequest hr = pending.point.request(); + if(pending.point == hr.entity(4)) { + // The very first segment; dragging final point drags both + // tangent points. + Vector p0 = SK.GetEntity(hr.entity(1))->PointGetNum(), + p3 = SK.GetEntity(hr.entity(4))->PointGetNum(), + p1 = p0.ScaledBy(2.0/3).Plus(p3.ScaledBy(1.0/3)), + p2 = p0.ScaledBy(1.0/3).Plus(p3.ScaledBy(2.0/3)); + SK.GetEntity(hr.entity(1+1))->PointForceTo(p1); + SK.GetEntity(hr.entity(1+2))->PointForceTo(p2); + } else { + // A subsequent segment; dragging point drags only final + // tangent point. + int i = SK.GetEntity(hr.entity(0))->extraPoints; + Vector pn = SK.GetEntity(hr.entity(4+i))->PointGetNum(), + pnm2 = SK.GetEntity(hr.entity(2+i))->PointGetNum(), + pnm1 = (pn.Plus(pnm2)).ScaledBy(0.5); + SK.GetEntity(hr.entity(3+i))->PointForceTo(pnm1); + } + + orig.mouse = mp; + SS.MarkGroupDirtyByEntity(pending.point); + break; + } + case Pending::DRAGGING_NEW_ARC_POINT: { + UpdateDraggedPoint(pending.point, x, y); + HitTestMakeSelection(mp); + + hRequest hr = pending.point.request(); + Vector ona = SK.GetEntity(hr.entity(2))->PointGetNum(); + Vector onb = SK.GetEntity(hr.entity(3))->PointGetNum(); + Vector center = (ona.Plus(onb)).ScaledBy(0.5); + + SK.GetEntity(hr.entity(1))->PointForceTo(center); + + orig.mouse = mp; + SS.MarkGroupDirtyByEntity(pending.point); + break; + } + case Pending::DRAGGING_NEW_RADIUS: + case Pending::DRAGGING_RADIUS: { + Entity *circle = SK.GetEntity(pending.circle); + Vector center = SK.GetEntity(circle->point[0])->PointGetNum(); + Point2d c2 = ProjectPoint(center); + double r = c2.DistanceTo(mp)/scale; + SK.GetEntity(circle->distance)->DistanceForceTo(r); + + SS.MarkGroupDirtyByEntity(pending.circle); + SS.ScheduleShowTW(); + break; + } + + case Pending::DRAGGING_NORMAL: { + Entity *normal = SK.GetEntity(pending.normal); + Vector p = SK.GetEntity(normal->point[0])->PointGetNum(); + Point2d p2 = ProjectPoint(p); + + Quaternion q = normal->NormalGetNum(); + Vector u = q.RotationU(), v = q.RotationV(); + + if(ctrlDown) { + double theta = atan2(orig.mouse.y-p2.y, orig.mouse.x-p2.x); + theta -= atan2(y-p2.y, x-p2.x); + + Vector normal = projRight.Cross(projUp); + u = u.RotatedAbout(normal, -theta); + v = v.RotatedAbout(normal, -theta); + } else { + double dx = -(x - orig.mouse.x); + double dy = -(y - orig.mouse.y); + double s = 0.3*(PI/180); // degrees per pixel + u = u.RotatedAbout(projUp, -s*dx); + u = u.RotatedAbout(projRight, s*dy); + v = v.RotatedAbout(projUp, -s*dx); + v = v.RotatedAbout(projRight, s*dy); + } + orig.mouse = mp; + normal->NormalForceTo(Quaternion::From(u, v)); + + SS.MarkGroupDirtyByEntity(pending.normal); + break; + } + + case Pending::DRAGGING_MARQUEE: + orig.mouse = mp; + Invalidate(); + return; + + case Pending::NONE: + case Pending::COMMAND: + ssassert(false, "Unexpected pending operation"); + } +} + +void GraphicsWindow::ClearPending(bool scheduleShowTW) { + pending.points.Clear(); + pending.requests.Clear(); + pending = {}; + if(scheduleShowTW) { + SS.ScheduleShowTW(); + } +} + +bool GraphicsWindow::IsFromPending(hRequest r) { + for(auto &req : pending.requests) { + if(req == r) return true; + } + return false; +} + +void GraphicsWindow::AddToPending(hRequest r) { + pending.requests.Add(&r); +} + +void GraphicsWindow::ReplacePending(hRequest before, hRequest after) { + for(auto &req : pending.requests) { + if(req == before) { + req = after; + } + } +} + +void GraphicsWindow::MouseMiddleOrRightDown(double x, double y) { + if(window->IsEditorVisible()) return; + + orig.offset = offset; + orig.projUp = projUp; + orig.projRight = projRight; + orig.mouse.x = x; + orig.mouse.y = y; + orig.startedMoving = false; +} + +void GraphicsWindow::MouseRightUp(double x, double y) { + SS.extraLine.draw = false; + Invalidate(); + + // Don't show a context menu if the user is right-clicking the toolbar, + // or if they are finishing a pan. + if(ToolbarMouseMoved((int)x, (int)y)) return; + if(orig.startedMoving) return; + + if(context.active) return; + + if(pending.operation == Pending::DRAGGING_NEW_LINE_POINT && pending.hasSuggestion) { + Constraint::TryConstrain(SS.GW.pending.suggestion, + Entity::NO_ENTITY, Entity::NO_ENTITY, pending.request.entity(0)); + } + + if(pending.operation == Pending::DRAGGING_NEW_LINE_POINT || + pending.operation == Pending::DRAGGING_NEW_CUBIC_POINT || + pending.operation == Pending::DRAGGING_NEW_ARC_POINT || + pending.operation == Pending::DRAGGING_NEW_RADIUS || + pending.operation == Pending::DRAGGING_NEW_POINT + ) + { + // Special case; use a right click to stop drawing lines, since + // a left click would draw another one. This is quicker and more + // intuitive than hitting escape. Likewise for other entities + // for consistency. + ClearPending(); + return; + } + + // The current mouse location + Vector v = offset.ScaledBy(-1); + v = v.Plus(projRight.ScaledBy(x/scale)); + v = v.Plus(projUp.ScaledBy(y/scale)); + + Platform::MenuRef menu = Platform::CreateMenu(); + context.active = true; + + if(!hover.IsEmpty()) { + MakeSelected(&hover); + SS.ScheduleShowTW(); + } + GroupSelection(); + + bool itemsSelected = (gs.n > 0 || gs.constraints > 0); + if(itemsSelected) { + if(gs.stylables > 0) { + Platform::MenuRef styleMenu = menu->AddSubMenu(_("Assign to Style")); + + bool empty = true; + for(const Style &s : SK.style) { + if(s.h.v < Style::FIRST_CUSTOM) continue; + + uint32_t v = s.h.v; + + styleMenu->AddItem(s.DescriptionString(), [v]() { + Style::AssignSelectionToStyle(v); + }); + empty = false; + } + + if(!empty) styleMenu->AddSeparator(); + + styleMenu->AddItem(_("No Style"), []() { + Style::AssignSelectionToStyle(0); + }); + styleMenu->AddItem(_("Newly Created Custom Style..."), [this]() { + uint32_t vs = Style::CreateCustomStyle(); + Style::AssignSelectionToStyle(vs); + ForceTextWindowShown(); + }); + } + if(gs.n + gs.constraints == 1) { + menu->AddItem(_("Group Info"), [this]() { + hGroup hg; + if(gs.entities == 1) { + hg = SK.GetEntity(gs.entity[0])->group; + } else if(gs.points == 1) { + hg = SK.GetEntity(gs.point[0])->group; + } else if(gs.constraints == 1) { + hg = SK.GetConstraint(gs.constraint[0])->group; + } else { + return; + } + ClearSelection(); + + SS.TW.GoToScreen(TextWindow::Screen::GROUP_INFO); + SS.TW.shown.group = hg; + SS.ScheduleShowTW(); + ForceTextWindowShown(); + }); + } + if(gs.n + gs.constraints == 1 && gs.stylables == 1) { + menu->AddItem(_("Style Info"), [this]() { + hStyle hs; + if(gs.entities == 1) { + hs = Style::ForEntity(gs.entity[0]); + } else if(gs.points == 1) { + hs = Style::ForEntity(gs.point[0]); + } else if(gs.constraints == 1) { + hs = SK.GetConstraint(gs.constraint[0])->GetStyle(); + } else { + return; + } + ClearSelection(); + + SS.TW.GoToScreen(TextWindow::Screen::STYLE_INFO); + SS.TW.shown.style = hs; + SS.ScheduleShowTW(); + ForceTextWindowShown(); + }); + } + if(gs.withEndpoints > 0) { + menu->AddItem(_("Select Edge Chain"), + []() { MenuEdit(Command::SELECT_CHAIN); }); + } + if(gs.constraints == 1 && gs.n == 0) { + Constraint *c = SK.GetConstraint(gs.constraint[0]); + if(c->HasLabel() && c->type != Constraint::Type::COMMENT) { + menu->AddItem(_("Toggle Reference Dimension"), + []() { Constraint::MenuConstrain(Command::REFERENCE); }); + } + if(c->type == Constraint::Type::ANGLE || + c->type == Constraint::Type::EQUAL_ANGLE) + { + menu->AddItem(_("Other Supplementary Angle"), + []() { Constraint::MenuConstrain(Command::OTHER_ANGLE); }); + } + } + if(gs.constraintLabels > 0 || gs.points > 0) { + menu->AddItem(_("Snap to Grid"), + []() { MenuEdit(Command::SNAP_TO_GRID); }); + } + + if(gs.points == 1 && gs.point[0].isFromRequest()) { + Request *r = SK.GetRequest(gs.point[0].request()); + int index = r->IndexOfPoint(gs.point[0]); + if((r->type == Request::Type::CUBIC && (index > 1 && index < r->extraPoints + 2)) || + r->type == Request::Type::CUBIC_PERIODIC) { + menu->AddItem(_("Remove Spline Point"), [this, r]() { + int index = r->IndexOfPoint(gs.point[0]); + ssassert(r->extraPoints != 0, + "Expected a bezier with interior control points"); + + SS.UndoRemember(); + Entity *e = SK.GetEntity(r->h.entity(0)); + + // First, fix point-coincident constraints involving this point. + // Then, remove all other constraints, since they would otherwise + // jump to an adjacent one and mess up the bezier after generation. + FixConstraintsForPointBeingDeleted(e->point[index]); + RemoveConstraintsForPointBeingDeleted(e->point[index]); + + for(int i = index; i < MAX_POINTS_IN_ENTITY - 1; i++) { + if(e->point[i + 1].v == 0) break; + Entity *p0 = SK.GetEntity(e->point[i]); + Entity *p1 = SK.GetEntity(e->point[i + 1]); + ReplacePointInConstraints(p1->h, p0->h); + p0->PointForceTo(p1->PointGetNum()); + } + r->extraPoints--; + SS.MarkGroupDirtyByEntity(gs.point[0]); + ClearSelection(); + }); + } + } + if(gs.entities == 1 && gs.entity[0].isFromRequest()) { + Request *r = SK.GetRequest(gs.entity[0].request()); + if(r->type == Request::Type::CUBIC || r->type == Request::Type::CUBIC_PERIODIC) { + Entity *e = SK.GetEntity(gs.entity[0]); + int addAfterPoint = e->GetPositionOfPoint(GetCamera(), Point2d::From(x, y)); + ssassert(addAfterPoint != -1, "Expected a nearest bezier point to be located"); + // Skip derivative point. + if(r->type == Request::Type::CUBIC) addAfterPoint++; + menu->AddItem(_("Add Spline Point"), [this, r, addAfterPoint, v]() { + int pointCount = r->extraPoints + + ((r->type == Request::Type::CUBIC_PERIODIC) ? 3 : 4); + if(pointCount >= MAX_POINTS_IN_ENTITY) { + Error(_("Cannot add spline point: maximum number of points reached.")); + return; + } + + SS.UndoRemember(); + r->extraPoints++; + SS.MarkGroupDirtyByEntity(gs.entity[0]); + SS.GenerateAll(SolveSpaceUI::Generate::REGEN); + + Entity *e = SK.GetEntity(r->h.entity(0)); + for(int i = MAX_POINTS_IN_ENTITY; i > addAfterPoint + 1; i--) { + Entity *p0 = SK.entity.FindByIdNoOops(e->point[i]); + if(p0 == NULL) continue; + Entity *p1 = SK.GetEntity(e->point[i - 1]); + ReplacePointInConstraints(p1->h, p0->h); + p0->PointForceTo(p1->PointGetNum()); + } + Entity *p = SK.GetEntity(e->point[addAfterPoint + 1]); + p->PointForceTo(v); + SS.MarkGroupDirtyByEntity(gs.entity[0]); + ClearSelection(); + }); + } + } + if(gs.entities == gs.n) { + menu->AddItem(_("Toggle Construction"), + []() { MenuRequest(Command::CONSTRUCTION); }); + } + + if(gs.points == 1) { + Entity *p = SK.GetEntity(gs.point[0]); + Constraint *c = nullptr; + IdList *lc = &(SK.constraint); + for(Constraint &ci : *lc) { + if(ci.type != Constraint::Type::POINTS_COINCIDENT) continue; + if(ci.ptA == p->h || ci.ptB == p->h) { + c = &ci; + break; + } + } + if(c) { + menu->AddItem(_("Delete Point-Coincident Constraint"), [this, p]() { + if(!p->IsPoint()) return; + + SS.UndoRemember(); + SK.constraint.ClearTags(); + for(Constraint &c : SK.constraint) { + if(c.type != Constraint::Type::POINTS_COINCIDENT) continue; + if(c.ptA == p->h || c.ptB == p->h) { + c.tag = 1; + } + } + SK.constraint.RemoveTagged(); + ClearSelection(); + }); + } + } + menu->AddSeparator(); + if(LockedInWorkplane()) { + menu->AddItem(_("Cut"), + []() { MenuClipboard(Command::CUT); }); + menu->AddItem(_("Copy"), + []() { MenuClipboard(Command::COPY); }); + } + } else { + menu->AddItem(_("Select All"), + []() { MenuEdit(Command::SELECT_ALL); }); + } + + if((!SS.clipboard.r.IsEmpty() || !SS.clipboard.c.IsEmpty()) && LockedInWorkplane()) { + menu->AddItem(_("Paste"), + []() { MenuClipboard(Command::PASTE); }); + menu->AddItem(_("Paste Transformed..."), + []() { MenuClipboard(Command::PASTE_TRANSFORM); }); + } + + if(itemsSelected) { + menu->AddItem(_("Delete"), + []() { MenuClipboard(Command::DELETE); }); + menu->AddSeparator(); + menu->AddItem(_("Unselect All"), + []() { MenuEdit(Command::UNSELECT_ALL); }); + } + // If only one item is selected, then it must be the one that we just + // selected from the hovered item; in which case unselect all and hovered + // are equivalent. + if(!hover.IsEmpty() && selection.n > 1) { + menu->AddItem(_("Unselect Hovered"), [this] { + if(!hover.IsEmpty()) { + MakeUnselected(&hover, /*coincidentPointTrick=*/true); + } + }); + } + + if(itemsSelected) { + menu->AddSeparator(); + menu->AddItem(_("Zoom to Fit"), + []() { MenuView(Command::ZOOM_TO_FIT); }); + } + + menu->PopUp(); + + context.active = false; + SS.ScheduleShowTW(); +} + +hRequest GraphicsWindow::AddRequest(Request::Type type) { + return AddRequest(type, /*rememberForUndo=*/true); +} +hRequest GraphicsWindow::AddRequest(Request::Type type, bool rememberForUndo) { + if(rememberForUndo) SS.UndoRemember(); + + Request r = {}; + r.group = activeGroup; + Group *g = SK.GetGroup(activeGroup); + if(g->type == Group::Type::DRAWING_3D || g->type == Group::Type::DRAWING_WORKPLANE) { + r.construction = false; + } else { + r.construction = true; + } + r.workplane = ActiveWorkplane(); + r.type = type; + SK.request.AddAndAssignId(&r); + + // We must regenerate the parameters, so that the code that tries to + // place this request's entities where the mouse is can do so. But + // we mustn't try to solve until reasonable values have been supplied + // for these new parameters, or else we'll get a numerical blowup. + r.Generate(&SK.entity, &SK.param); + SS.MarkGroupDirty(r.group); + return r.h; +} + +Vector GraphicsWindow::SnapToEntityByScreenPoint(Point2d pp, hEntity he) { + Entity *e = SK.GetEntity(he); + if(e->IsPoint()) return e->PointGetNum(); + SEdgeList *edges = e->GetOrGenerateEdges(); + + double minD = -1.0f; + double k = 0.0; + const SEdge *edge = NULL; + for(const auto &e : edges->l) { + Point2d p0 = ProjectPoint(e.a); + Point2d p1 = ProjectPoint(e.b); + Point2d dir = p1.Minus(p0); + double d = pp.DistanceToLine(p0, dir, /*asSegment=*/true); + if(minD > 0.0 && d > minD) continue; + minD = d; + k = pp.Minus(p0).Dot(dir) / dir.Dot(dir); + edge = &e; + } + if(edge == NULL) return UnProjectPoint(pp); + return edge->a.Plus(edge->b.Minus(edge->a).ScaledBy(k)); +} + +bool GraphicsWindow::ConstrainPointByHovered(hEntity pt, const Point2d *projected) { + if(!hover.entity.v) return false; + + Entity *point = SK.GetEntity(pt); + Entity *e = SK.GetEntity(hover.entity); + if(e->IsPoint()) { + point->PointForceTo(e->PointGetNum()); + Constraint::ConstrainCoincident(e->h, pt); + return true; + } + if(e->IsCircle()) { + if(projected != NULL) { + Vector snapPos = SnapToEntityByScreenPoint(*projected, e->h); + point->PointForceTo(snapPos); + } + Constraint::Constrain(Constraint::Type::PT_ON_CIRCLE, + pt, Entity::NO_ENTITY, e->h); + return true; + } + if(e->type == Entity::Type::LINE_SEGMENT) { + if(projected != NULL) { + Vector snapPos = SnapToEntityByScreenPoint(*projected, e->h); + point->PointForceTo(snapPos); + } + Constraint::Constrain(Constraint::Type::PT_ON_LINE, + pt, Entity::NO_ENTITY, e->h); + return true; + } + + return false; +} + +bool GraphicsWindow::MouseEvent(Platform::MouseEvent event) { + using Platform::MouseEvent; + double width, height; + window->GetContentSize(&width, &height); + + event.x = event.x - width / 2; + event.y = height / 2 - event.y; + + switch(event.type) { + case MouseEvent::Type::MOTION: + this->MouseMoved(event.x, event.y, + event.button == MouseEvent::Button::LEFT, + event.button == MouseEvent::Button::MIDDLE, + event.button == MouseEvent::Button::RIGHT, + event.shiftDown, + event.controlDown); + break; + + case MouseEvent::Type::PRESS: + if(event.button == MouseEvent::Button::LEFT) { + this->MouseLeftDown(event.x, event.y, event.shiftDown, event.controlDown); + } else if(event.button == MouseEvent::Button::MIDDLE || + event.button == MouseEvent::Button::RIGHT) { + this->MouseMiddleOrRightDown(event.x, event.y); + } + break; + + case MouseEvent::Type::DBL_PRESS: + if(event.button == MouseEvent::Button::LEFT) { + this->MouseLeftDoubleClick(event.x, event.y); + } + break; + + case MouseEvent::Type::RELEASE: + if(event.button == MouseEvent::Button::LEFT) { + this->MouseLeftUp(event.x, event.y, event.shiftDown, event.controlDown); + } else if(event.button == MouseEvent::Button::RIGHT) { + this->MouseRightUp(event.x, event.y); + } + break; + + case MouseEvent::Type::SCROLL_VERT: + this->MouseScroll(event.shiftDown ? event.scrollDelta / 10 : event.scrollDelta); + break; + + case MouseEvent::Type::LEAVE: + this->MouseLeave(); + break; + } + + return true; +} + +void GraphicsWindow::MouseLeftDown(double mx, double my, bool shiftDown, bool ctrlDown) { + orig.mouseDown = true; + + if(window->IsEditorVisible()) { + orig.mouse = Point2d::From(mx, my); + orig.mouseOnButtonDown = orig.mouse; + window->HideEditor(); + return; + } + SS.TW.HideEditControl(); + + if(SS.showToolbar) { + if(ToolbarMouseDown((int)mx, (int)my)) return; + } + + // This will be clobbered by MouseMoved below. + bool hasConstraintSuggestion = pending.hasSuggestion; + Constraint::Type constraintSuggestion = pending.suggestion; + + // Make sure the hover is up to date. + MouseMoved(mx, my, /*leftDown=*/false, /*middleDown=*/false, /*rightDown=*/false, + /*shiftDown=*/false, /*ctrlDown=*/false); + orig.mouse.x = mx; + orig.mouse.y = my; + orig.mouseOnButtonDown = orig.mouse; + Point2d mouse = Point2d::From(mx, my); + + // The current mouse location + Vector v = offset.ScaledBy(-1); + v = v.Plus(projRight.ScaledBy(mx/scale)); + v = v.Plus(projUp.ScaledBy(my/scale)); + + hRequest hr = {}; + hConstraint hc = {}; + switch(pending.operation) { + case Pending::COMMAND: + switch(pending.command) { + case Command::DATUM_POINT: + hr = AddRequest(Request::Type::DATUM_POINT); + SK.GetEntity(hr.entity(0))->PointForceTo(v); + ConstrainPointByHovered(hr.entity(0), &mouse); + + ClearSuper(); + break; + + case Command::LINE_SEGMENT: + case Command::CONSTR_SEGMENT: + hr = AddRequest(Request::Type::LINE_SEGMENT); + SK.GetRequest(hr)->construction = (pending.command == Command::CONSTR_SEGMENT); + SK.GetEntity(hr.entity(1))->PointForceTo(v); + ConstrainPointByHovered(hr.entity(1), &mouse); + + ClearSuper(); + AddToPending(hr); + + pending.operation = Pending::DRAGGING_NEW_LINE_POINT; + pending.request = hr; + pending.point = hr.entity(2); + pending.description = _("click next point of line, or press Esc"); + SK.GetEntity(pending.point)->PointForceTo(v); + break; + + case Command::RECTANGLE: { + if(!SS.GW.LockedInWorkplane()) { + Error(_("Can't draw rectangle in 3d; first, activate a workplane " + "with Sketch -> In Workplane.")); + ClearSuper(); + break; + } + hRequest lns[4]; + int i; + SS.UndoRemember(); + for(i = 0; i < 4; i++) { + lns[i] = AddRequest(Request::Type::LINE_SEGMENT, /*rememberForUndo=*/false); + AddToPending(lns[i]); + } + for(i = 0; i < 4; i++) { + Constraint::ConstrainCoincident( + lns[i].entity(1), lns[(i+1)%4].entity(2)); + SK.GetEntity(lns[i].entity(1))->PointForceTo(v); + SK.GetEntity(lns[i].entity(2))->PointForceTo(v); + } + for(i = 0; i < 4; i++) { + Constraint::Constrain( + (i % 2) ? Constraint::Type::HORIZONTAL : Constraint::Type::VERTICAL, + Entity::NO_ENTITY, Entity::NO_ENTITY, + lns[i].entity(0)); + } + if(ConstrainPointByHovered(lns[2].entity(1), &mouse)) { + Vector pos = SK.GetEntity(lns[2].entity(1))->PointGetNum(); + for(i = 0; i < 4; i++) { + SK.GetEntity(lns[i].entity(1))->PointForceTo(pos); + SK.GetEntity(lns[i].entity(2))->PointForceTo(pos); + } + } + + pending.operation = Pending::DRAGGING_NEW_POINT; + pending.point = lns[1].entity(2); + pending.description = _("click to place other corner of rectangle"); + hr = lns[0]; + break; + } + case Command::CIRCLE: + hr = AddRequest(Request::Type::CIRCLE); + // Centered where we clicked + SK.GetEntity(hr.entity(1))->PointForceTo(v); + // Normal to the screen + SK.GetEntity(hr.entity(32))->NormalForceTo( + Quaternion::From(SS.GW.projRight, SS.GW.projUp)); + // Initial radius zero + SK.GetEntity(hr.entity(64))->DistanceForceTo(0); + + ConstrainPointByHovered(hr.entity(1), &mouse); + + ClearSuper(); + AddToPending(hr); + + pending.operation = Pending::DRAGGING_NEW_RADIUS; + pending.circle = hr.entity(0); + pending.description = _("click to set radius"); + break; + + case Command::ARC: { + if(!SS.GW.LockedInWorkplane()) { + Error(_("Can't draw arc in 3d; first, activate a workplane " + "with Sketch -> In Workplane.")); + ClearPending(); + break; + } + hr = AddRequest(Request::Type::ARC_OF_CIRCLE); + // This fudge factor stops us from immediately failing to solve + // because of the arc's implicit (equal radius) tangent. + Vector adj = SS.GW.projRight.WithMagnitude(2/SS.GW.scale); + SK.GetEntity(hr.entity(1))->PointForceTo(v.Minus(adj)); + SK.GetEntity(hr.entity(2))->PointForceTo(v); + SK.GetEntity(hr.entity(3))->PointForceTo(v); + ConstrainPointByHovered(hr.entity(2), &mouse); + + ClearSuper(); + AddToPending(hr); + + pending.operation = Pending::DRAGGING_NEW_ARC_POINT; + pending.point = hr.entity(3); + pending.description = _("click to place point"); + break; + } + case Command::CUBIC: + hr = AddRequest(Request::Type::CUBIC); + SK.GetEntity(hr.entity(1))->PointForceTo(v); + SK.GetEntity(hr.entity(2))->PointForceTo(v); + SK.GetEntity(hr.entity(3))->PointForceTo(v); + SK.GetEntity(hr.entity(4))->PointForceTo(v); + ConstrainPointByHovered(hr.entity(1), &mouse); + + ClearSuper(); + AddToPending(hr); + + pending.operation = Pending::DRAGGING_NEW_CUBIC_POINT; + pending.point = hr.entity(4); + pending.description = _("click next point of cubic, or press Esc"); + break; + + case Command::WORKPLANE: + if(LockedInWorkplane()) { + Error(_("Sketching in a workplane already; sketch in 3d before " + "creating new workplane.")); + ClearSuper(); + break; + } + hr = AddRequest(Request::Type::WORKPLANE); + SK.GetEntity(hr.entity(1))->PointForceTo(v); + SK.GetEntity(hr.entity(32))->NormalForceTo( + Quaternion::From(SS.GW.projRight, SS.GW.projUp)); + ConstrainPointByHovered(hr.entity(1), &mouse); + + ClearSuper(); + break; + + case Command::TTF_TEXT: { + if(!SS.GW.LockedInWorkplane()) { + Error(_("Can't draw text in 3d; first, activate a workplane " + "with Sketch -> In Workplane.")); + ClearSuper(); + break; + } + hr = AddRequest(Request::Type::TTF_TEXT); + AddToPending(hr); + Request *r = SK.GetRequest(hr); + r->str = "Abc"; + r->font = Platform::embeddedFont; + + for(int i = 1; i <= 4; i++) { + SK.GetEntity(hr.entity(i))->PointForceTo(v); + } + + pending.operation = Pending::DRAGGING_NEW_POINT; + pending.point = hr.entity(3); + pending.description = _("click to place bottom right of text"); + break; + } + + case Command::IMAGE: { + if(!SS.GW.LockedInWorkplane()) { + Error(_("Can't draw image in 3d; first, activate a workplane " + "with Sketch -> In Workplane.")); + ClearSuper(); + break; + } + hr = AddRequest(Request::Type::IMAGE); + AddToPending(hr); + Request *r = SK.GetRequest(hr); + r->file = pending.filename; + r->construction = true; + + for(int i = 1; i <= 4; i++) { + SK.GetEntity(hr.entity(i))->PointForceTo(v); + } + + pending.operation = Pending::DRAGGING_NEW_POINT; + pending.point = hr.entity(3); + pending.description = "click to place bottom right of image"; + break; + } + + case Command::COMMENT: { + ClearSuper(); + Constraint c = {}; + c.group = SS.GW.activeGroup; + c.workplane = SS.GW.ActiveWorkplane(); + c.type = Constraint::Type::COMMENT; + c.disp.offset = v; + c.comment = _("NEW COMMENT -- DOUBLE-CLICK TO EDIT"); + hc = Constraint::AddConstraint(&c); + break; + } + default: ssassert(false, "Unexpected pending menu id"); + } + break; + + case Pending::DRAGGING_RADIUS: + ClearPending(); + break; + + case Pending::DRAGGING_NEW_POINT: + case Pending::DRAGGING_NEW_ARC_POINT: + ConstrainPointByHovered(pending.point, &mouse); + ClearPending(); + break; + + case Pending::DRAGGING_NEW_CUBIC_POINT: { + hRequest hr = pending.point.request(); + Request *r = SK.GetRequest(hr); + + if(hover.entity == hr.entity(1) && r->extraPoints >= 2) { + // They want the endpoints coincident, which means a periodic + // spline instead. + r->type = Request::Type::CUBIC_PERIODIC; + // Remove the off-curve control points, which are no longer + // needed here; so move [2,ep+1] down, skipping first pt. + int i; + for(i = 2; i <= r->extraPoints+1; i++) { + SK.GetEntity(hr.entity((i-1)+1))->PointForceTo( + SK.GetEntity(hr.entity(i+1))->PointGetNum()); + } + // and move ep+3 down by two, skipping both + SK.GetEntity(hr.entity((r->extraPoints+1)+1))->PointForceTo( + SK.GetEntity(hr.entity((r->extraPoints+3)+1))->PointGetNum()); + r->extraPoints -= 2; + // And we're done. + SS.MarkGroupDirty(r->group); + ClearPending(); + break; + } + + if(ConstrainPointByHovered(pending.point, &mouse)) { + ClearPending(); + break; + } + + Entity e; + if(r->extraPoints >= (int)arraylen(e.point) - 4) { + ClearPending(); + break; + } + + (SK.GetRequest(hr)->extraPoints)++; + SS.GenerateAll(SolveSpaceUI::Generate::REGEN); + + int ep = r->extraPoints; + Vector last = SK.GetEntity(hr.entity(3+ep))->PointGetNum(); + + SK.GetEntity(hr.entity(2+ep))->PointForceTo(last); + SK.GetEntity(hr.entity(3+ep))->PointForceTo(v); + SK.GetEntity(hr.entity(4+ep))->PointForceTo(v); + pending.point = hr.entity(4+ep); + break; + } + + case Pending::DRAGGING_NEW_LINE_POINT: { + if(hover.entity.v) { + Entity *e = SK.GetEntity(hover.entity); + if(e->IsPoint()) { + hRequest hrl = pending.point.request(); + Entity *sp = SK.GetEntity(hrl.entity(1)); + if(( e->PointGetNum()).Equals( + (sp->PointGetNum()))) + { + // If we constrained by the hovered point, then we + // would create a zero-length line segment. That's + // not good, so just stop drawing. + ClearPending(); + break; + } + } + } + + bool doneDragging = ConstrainPointByHovered(pending.point, &mouse); + + // Constrain the line segment horizontal or vertical if close enough + if(hasConstraintSuggestion) { + Constraint::TryConstrain(constraintSuggestion, + Entity::NO_ENTITY, Entity::NO_ENTITY, pending.request.entity(0)); + } + + if(doneDragging) { + ClearPending(); + break; + } + + // Create a new line segment, so that we continue drawing. + hRequest hr = AddRequest(Request::Type::LINE_SEGMENT); + ReplacePending(pending.request, hr); + SK.GetRequest(hr)->construction = SK.GetRequest(pending.request)->construction; + // Displace the second point of the new line segment slightly, + // to avoid creating zero-length edge warnings. + SK.GetEntity(hr.entity(2))->PointForceTo( + v.Plus(projRight.ScaledBy(0.5/scale))); + + // Constrain the line segments to share an endpoint + Constraint::ConstrainCoincident(pending.point, hr.entity(1)); + Vector pendingPos = SK.GetEntity(pending.point)->PointGetNum(); + SK.GetEntity(hr.entity(1))->PointForceTo(pendingPos); + + // And drag an endpoint of the new line segment + pending.operation = Pending::DRAGGING_NEW_LINE_POINT; + pending.request = hr; + pending.point = hr.entity(2); + pending.description = _("click next point of line, or press Esc"); + + break; + } + + case Pending::NONE: + default: + ClearPending(); + if(!hover.IsEmpty()) { + if(!ctrlDown) { + hoverWasSelectedOnMousedown = IsSelected(&hover); + MakeSelected(&hover); + } else { + MakeUnselected(&hover, /*coincidentPointTrick=*/true); + } + } + break; + } + + // Activate group with newly created request/constraint + Group *g = NULL; + if(hr.v != 0) { + g = SK.GetGroup(SK.GetRequest(hr)->group); + } + if(hc.v != 0) { + g = SK.GetGroup(SK.GetConstraint(hc)->group); + } + if(g != NULL) { + g->visible = true; + } + + SS.ScheduleShowTW(); + Invalidate(); +} + +void GraphicsWindow::MouseLeftUp(double mx, double my, bool shiftDown, bool ctrlDown) { + orig.mouseDown = false; + + switch(pending.operation) { + case Pending::DRAGGING_POINTS: + case Pending::DRAGGING_CONSTRAINT: + case Pending::DRAGGING_NORMAL: + case Pending::DRAGGING_RADIUS: + if(!hoverWasSelectedOnMousedown) { + // And then clear the selection again, since they + // probably didn't want that selected if they just + // were dragging it. + ClearSelection(); + } + hoverWasSelectedOnMousedown = false; + SS.extraLine.draw = false; + ClearPending(); + Invalidate(); + break; + + case Pending::DRAGGING_MARQUEE: + SelectByMarquee(); + ClearPending(); + Invalidate(); + break; + + case Pending::NONE: + if(hover.IsEmpty() && !ctrlDown) { + ClearSelection(); + } + break; + + default: + break; // do nothing + } +} + +void GraphicsWindow::EditConstraint(hConstraint constraint) { + constraintBeingEdited = constraint; + ClearSuper(); + + Constraint *c = SK.GetConstraint(constraintBeingEdited); + if(!c->HasLabel()) { + // Not meaningful to edit a constraint without a dimension + return; + } + if(c->reference) { + // Not meaningful to edit a reference dimension + return; + } + + Vector p3 = c->GetLabelPos(GetCamera()); + Point2d p2 = ProjectPoint(p3); + + std::string editValue; + std::string editPlaceholder; + switch(c->type) { + case Constraint::Type::COMMENT: + editValue = c->comment; + editPlaceholder = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + break; + + default: { + double value = fabs(c->valA); + + // If displayed as radius, also edit as radius. + if(c->type == Constraint::Type::DIAMETER && c->other) + value /= 2; + + // Try showing value with default number of digits after decimal first. + if(c->type == Constraint::Type::LENGTH_RATIO || c->type == Constraint::Type::ARC_ARC_LEN_RATIO || c->type == Constraint::Type::ARC_LINE_LEN_RATIO) { + editValue = ssprintf("%.3f", value); + } else if(c->type == Constraint::Type::ANGLE) { + editValue = SS.DegreeToString(value); + } else { + editValue = SS.MmToString(value, true); + value /= SS.MmPerUnit(); + } + // If that's not enough to represent it exactly, show the value with as many + // digits after decimal as required, up to 10. + int digits = 0; + while(fabs(std::stod(editValue) - value) > 1e-10) { + editValue = ssprintf("%.*f", digits, value); + digits++; + } + editPlaceholder = "10.000000"; + break; + } + } + + double width, height; + window->GetContentSize(&width, &height); + hStyle hs = c->disp.style; + if(hs.v == 0) hs.v = Style::CONSTRAINT; + double capHeight = Style::TextHeight(hs); + double fontHeight = VectorFont::Builtin()->GetHeight(capHeight); + double editMinWidth = VectorFont::Builtin()->GetWidth(capHeight, editPlaceholder); + window->ShowEditor(p2.x + width / 2, height / 2 - p2.y, + fontHeight, editMinWidth, + /*isMonospace=*/false, editValue); +} + +void GraphicsWindow::MouseLeftDoubleClick(double mx, double my) { + if(window->IsEditorVisible()) return; + SS.TW.HideEditControl(); + + if(hover.constraint.v) { + EditConstraint(hover.constraint); + } +} + +void GraphicsWindow::EditControlDone(const std::string &s) { + window->HideEditor(); + window->Invalidate(); + + Constraint *c = SK.GetConstraint(constraintBeingEdited); + + if(c->type == Constraint::Type::COMMENT) { + SS.UndoRemember(); + c->comment = s; + return; + } + + if(Expr *e = Expr::From(s, true)) { + SS.UndoRemember(); + + switch(c->type) { + case Constraint::Type::PROJ_PT_DISTANCE: + case Constraint::Type::PT_LINE_DISTANCE: + case Constraint::Type::PT_FACE_DISTANCE: + case Constraint::Type::PT_PLANE_DISTANCE: + case Constraint::Type::LENGTH_DIFFERENCE: + case Constraint::Type::ARC_ARC_DIFFERENCE: + case Constraint::Type::ARC_LINE_DIFFERENCE: { + // The sign is not displayed to the user, but this is a signed + // distance internally. To flip the sign, the user enters a + // negative distance. + bool wasNeg = (c->valA < 0); + if(wasNeg) { + c->valA = -SS.ExprToMm(e); + } else { + c->valA = SS.ExprToMm(e); + } + break; + } + case Constraint::Type::ANGLE: + case Constraint::Type::LENGTH_RATIO: + case Constraint::Type::ARC_ARC_LEN_RATIO: + case Constraint::Type::ARC_LINE_LEN_RATIO: + // These don't get the units conversion for distance, and + // they're always positive + c->valA = fabs(e->Eval()); + break; + + case Constraint::Type::DIAMETER: + c->valA = fabs(SS.ExprToMm(e)); + + // If displayed and edited as radius, convert back + // to diameter + if(c->other) + c->valA *= 2; + break; + + default: + // These are always positive, and they get the units conversion. + c->valA = fabs(SS.ExprToMm(e)); + break; + } + SS.MarkGroupDirty(c->group); + } +} + +void GraphicsWindow::MouseScroll(double zoomMultiplyer) { + // To support smooth scrolling where scroll wheel events come in increments + // smaller (or larger) than 1 we do: + // scale *= exp(ln(1.2) * zoomMultiplyer); + // to ensure that the same total scroll delta always results in the same + // total zoom irrespective of in how many increments the zoom was applied. + // For example if we scroll a total delta of a+b in two events vs. one then + // scale * e^a * e^b == scale * e^(a+b) + // while + // scale * a * b != scale * (a+b) + // So this constant is ln(1.2) = 0.1823216 to make the default zoom 1.2x + ZoomToMouse(zoomMultiplyer); +} + +void GraphicsWindow::MouseLeave() { + // Un-hover everything when the mouse leaves our window, unless there's + // currently a context menu shown. + if(!context.active) { + hover.Clear(); + toolbarHovered = Command::NONE; + Invalidate(); + } + SS.extraLine.draw = false; +} + +void GraphicsWindow::SixDofEvent(Platform::SixDofEvent event) { + if(event.type == Platform::SixDofEvent::Type::RELEASE) { + ZoomToFit(/*includingInvisibles=*/false, /*useSelection=*/true); + Invalidate(); + return; + } + + if(!havePainted) return; + Vector out = projRight.Cross(projUp); + + // rotation vector is axis of rotation, and its magnitude is angle + Vector aa = Vector::From(event.rotationX, event.rotationY, event.rotationZ); + // but it's given with respect to screen projection frame + aa = aa.ScaleOutOfCsys(projRight, projUp, out); + double aam = aa.Magnitude(); + if(aam > 0.0) aa = aa.WithMagnitude(1); + + // This can either transform our view, or transform a linked part. + GroupSelection(); + Entity *e = NULL; + Group *g = NULL; + if(gs.points == 1 && gs.n == 1) e = SK.GetEntity(gs.point [0]); + if(gs.entities == 1 && gs.n == 1) e = SK.GetEntity(gs.entity[0]); + if(e) g = SK.GetGroup(e->group); + if(g && g->type == Group::Type::LINKED && !event.shiftDown) { + // Apply the transformation to a linked part. Gain down the Z + // axis, since it's hard to see what you're doing on that one since + // it's normal to the screen. + Vector t = projRight.ScaledBy(event.translationX/scale).Plus( + projUp .ScaledBy(event.translationY/scale).Plus( + out .ScaledBy(0.1*event.translationZ/scale))); + Quaternion q = Quaternion::From(aa, aam); + + // If we go five seconds without SpaceNavigator input, or if we've + // switched groups, then consider that a new action and save an undo + // point. + int64_t now = GetMilliseconds(); + if(now - last6DofTime > 5000 || + last6DofGroup != g->h) + { + SS.UndoRemember(); + } + + g->TransformImportedBy(t, q); + + last6DofTime = now; + last6DofGroup = g->h; + SS.MarkGroupDirty(g->h); + } else { + // Apply the transformation to the view of the everything. The + // x and y components are translation; but z component is scale, + // not translation, or else it would do nothing in a parallel + // projection + offset = offset.Plus(projRight.ScaledBy(event.translationX/scale)); + offset = offset.Plus(projUp.ScaledBy(event.translationY/scale)); + scale *= exp(0.001*event.translationZ); + + if(aam > 0.0) { + projRight = projRight.RotatedAbout(aa, -aam); + projUp = projUp. RotatedAbout(aa, -aam); + NormalizeProjectionVectors(); + } + } + + havePainted = false; + Invalidate(); +} + +} // namespace SolveSpace diff --git a/src/param.h b/src/param.h new file mode 100644 index 0000000..a32aec8 --- /dev/null +++ b/src/param.h @@ -0,0 +1,52 @@ +#ifndef SOLVESPACE_PARAM_H +#define SOLVESPACE_PARAM_H + +#include +#include + +#include "handle.h" + +namespace SolveSpace { + +class hRequest; + +class hParam { +public: + // bits 15: 0 -- param index + // 31:16 -- request index + uint32_t v; + + inline hRequest request() const; +}; + +template<> +struct IsHandleOracle : std::true_type {}; + +class Param { +public: + int tag; + hParam h; + + double val; + bool known; + bool free; + + // Used only in the solver + Param *substd; + + static const hParam NO_PARAM; + + void Clear() {} +}; + +// Use a forward declaration in order to avoid pulling dsc.h in for units that +// don't need to use `ParamList` +template class IdList; + +using ParamList = IdList; + +using ParamSet = std::unordered_set>; + +} // namespace SolveSpace + +#endif // !SOLVESPACE_PARAM_H diff --git a/src/platform/entrycli.cpp b/src/platform/entrycli.cpp new file mode 100644 index 0000000..6284e12 --- /dev/null +++ b/src/platform/entrycli.cpp @@ -0,0 +1,399 @@ +//----------------------------------------------------------------------------- +// Our main() function for the command-line interface. +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- + +#include +#include + +#include "solvespace.h" +#include "config.h" + +using namespace SolveSpace; + +static void ShowUsage(const std::string &cmd) { + fprintf(stderr, "Usage: %s [filename...]", cmd.c_str()); +//-----------------------------------------------------------------------------> 80 col */ + fprintf(stderr, R"( + When run, performs an action specified by on every . + +Common options: + -o, --output + For an input file .slvs, replaces the '%%' symbol in + with and uses it as output file. For example, when using + --output %%-2d.png for input files f/a.slvs and f/b.slvs, output files + f/a-2d.png and f/b-2d.png will be written. + -v, --view + Selects the camera direction. can be one of "top", "bottom", + "left", "right", "front", "back", or "isometric". + -t, --chord-tol + Selects the chord tolerance, used for converting exact curves to + piecewise linear, and exact surfaces into triangle meshes. + For export commands, the unit is mm, and the default is 1.0 mm. + For non-export commands, the unit is %%, and the default is 1.0 %%. + -b, --bg-color + Whether to export the background colour in vector formats. Defaults to off. + +Commands: + version + Prints the current solvespace version. + thumbnail --output --size --view + [--chord-tol ] + Outputs a rendered view of the sketch, like the SolveSpace GUI would. + is x, in pixels. Graphics acceleration is + not used, and the output may look slightly different from the GUI. + export-view --output --view [--chord-tol ] + [--bg-color ] + Exports a view of the sketch, in a 2d vector format. + export-wireframe --output [--chord-tol ] + Exports a wireframe of the sketch, in a 3d vector format. + export-mesh --output [--chord-tol ] + Exports a triangle mesh of solids in the sketch, with exact surfaces + being triangulated first. + export-surfaces --output + Exports exact surfaces of solids in the sketch, if any. + regenerate [--chord-tol ] + Reloads all imported files, regenerates the sketch, and saves it. + Note that, although this is not an export command, it uses absolute + chord tolerance, and can be used to prepare assemblies for export. +)"); + + auto FormatListFromFileFilters = [](const std::vector &filters) { + std::string descr; + for(auto filter : filters) { + descr += "\n "; + descr += filter.name; + descr += " ("; + bool first = true; + for(auto extension : filter.extensions) { + if(!first) { + descr += ", "; + } + descr += extension; + first = false; + } + descr += ")"; + } + return descr; + }; + + fprintf(stderr, R"( +File formats: + thumbnail:%s + export-view:%s + export-wireframe:%s + export-mesh:%s + export-surfaces:%s +)", FormatListFromFileFilters(Platform::RasterFileFilters).c_str(), + FormatListFromFileFilters(Platform::VectorFileFilters).c_str(), + FormatListFromFileFilters(Platform::Vector3dFileFilters).c_str(), + FormatListFromFileFilters(Platform::MeshFileFilters).c_str(), + FormatListFromFileFilters(Platform::SurfaceFileFilters).c_str()); +} + +static bool RunCommand(const std::vector args) { + if(args.size() < 2) return false; + + for(const std::string &arg : args) { + if(arg == "--help" || arg == "-h") { + ShowUsage(args[0]); + return true; + } + } + + std::function runner; + + std::vector inputFiles; + auto ParseInputFile = [&](size_t &argn) { + std::string arg = args[argn]; + if(arg[0] != '-') { + inputFiles.push_back(Platform::Path::From(arg)); + return true; + } else return false; + }; + + std::string outputPattern; + auto ParseOutputPattern = [&](size_t &argn) { + if(argn + 1 < args.size() && (args[argn] == "--output" || + args[argn] == "-o")) { + argn++; + outputPattern = args[argn]; + return true; + } else return false; + }; + + Vector projUp = {}, projRight = {}; + auto ParseViewDirection = [&](size_t &argn) { + if(argn + 1 < args.size() && (args[argn] == "--view" || + args[argn] == "-v")) { + argn++; + if(args[argn] == "top") { + projRight = Vector::From(1, 0, 0); + projUp = Vector::From(0, 0, -1); + } else if(args[argn] == "bottom") { + projRight = Vector::From(1, 0, 0); + projUp = Vector::From(0, 0, 1); + } else if(args[argn] == "left") { + projRight = Vector::From(0, 0, 1); + projUp = Vector::From(0, 1, 0); + } else if(args[argn] == "right") { + projRight = Vector::From(0, 0, -1); + projUp = Vector::From(0, 1, 0); + } else if(args[argn] == "front") { + projRight = Vector::From(1, 0, 0); + projUp = Vector::From(0, 1, 0); + } else if(args[argn] == "back") { + projRight = Vector::From(-1, 0, 0); + projUp = Vector::From(0, 1, 0); + } else if(args[argn] == "isometric") { + projRight = Vector::From(0.707, 0.000, -0.707); + projUp = Vector::From(-0.408, 0.816, -0.408); + } else { + fprintf(stderr, "Unrecognized view direction '%s'\n", args[argn].c_str()); + } + return true; + } else return false; + }; + + double chordTol = 1.0; + auto ParseChordTolerance = [&](size_t &argn) { + if(argn + 1 < args.size() && (args[argn] == "--chord-tol" || + args[argn] == "-t")) { + argn++; + if(sscanf(args[argn].c_str(), "%lf", &chordTol) == 1) { + return true; + } else return false; + } else return false; + }; + + bool bg_color = false; + auto ParseBgColor = [&](size_t &argn) { + if(argn + 1 < args.size() && (args[argn] == "--bg-color" || + args[argn] == "-b")) { + argn++; + if(args[argn] == "on") { + bg_color = true; + return true; + } else if(args[argn] == "off") { + bg_color = false; + return true; + } else return false; + } else return false; + }; + + unsigned width = 0, height = 0; + if(args[1] == "version") { + fprintf(stderr, "SolveSpace version %s \n\n", PACKAGE_VERSION); + return false; + } else if(args[1] == "thumbnail") { + auto ParseSize = [&](size_t &argn) { + if(argn + 1 < args.size() && args[argn] == "--size") { + argn++; + if(sscanf(args[argn].c_str(), "%ux%u", &width, &height) == 2) { + return true; + } else return false; + } else return false; + }; + + for(size_t argn = 2; argn < args.size(); argn++) { + if(!(ParseInputFile(argn) || + ParseOutputPattern(argn) || + ParseViewDirection(argn) || + ParseChordTolerance(argn) || + ParseSize(argn))) { + fprintf(stderr, "Unrecognized option '%s'.\n", args[argn].c_str()); + return false; + } + } + + if(width == 0 || height == 0) { + fprintf(stderr, "Non-zero viewport size must be specified.\n"); + return false; + } + + if(EXACT(projUp.Magnitude() == 0 || projRight.Magnitude() == 0)) { + fprintf(stderr, "View direction must be specified.\n"); + return false; + } + + runner = [&](const Platform::Path &output) { + Camera camera = {}; + camera.pixelRatio = 1; + camera.gridFit = true; + camera.width = width; + camera.height = height; + camera.projUp = projUp; + camera.projRight = projRight; + + SS.GW.projUp = projUp; + SS.GW.projRight = projRight; + SS.GW.scale = SS.GW.ZoomToFit(camera); + camera.scale = SS.GW.scale; + camera.offset = SS.GW.offset; + SS.GenerateAll(); + + CairoPixmapRenderer pixmapCanvas; + pixmapCanvas.antialias = true; + pixmapCanvas.SetLighting(SS.GW.GetLighting()); + pixmapCanvas.SetCamera(camera); + pixmapCanvas.Init(); + + pixmapCanvas.StartFrame(); + SS.GW.Draw(&pixmapCanvas); + pixmapCanvas.FlushFrame(); + pixmapCanvas.FinishFrame(); + pixmapCanvas.ReadFrame()->WritePng(output, /*flip=*/true); + + pixmapCanvas.Clear(); + }; + } else if(args[1] == "export-view") { + for(size_t argn = 2; argn < args.size(); argn++) { + if(!(ParseInputFile(argn) || + ParseOutputPattern(argn) || + ParseViewDirection(argn) || + ParseChordTolerance(argn) || + ParseBgColor(argn))) { + fprintf(stderr, "Unrecognized option '%s'.\n", args[argn].c_str()); + return false; + } + } + + if(EXACT(projUp.Magnitude() == 0 || projRight.Magnitude() == 0)) { + fprintf(stderr, "View direction must be specified.\n"); + return false; + } + + runner = [&](const Platform::Path &output) { + SS.GW.projRight = projRight; + SS.GW.projUp = projUp; + SS.exportChordTol = chordTol; + SS.exportBackgroundColor = bg_color; + + SS.ExportViewOrWireframeTo(output, /*exportWireframe=*/false); + }; + } else if(args[1] == "export-wireframe") { + for(size_t argn = 2; argn < args.size(); argn++) { + if(!(ParseInputFile(argn) || + ParseOutputPattern(argn) || + ParseChordTolerance(argn))) { + fprintf(stderr, "Unrecognized option '%s'.\n", args[argn].c_str()); + return false; + } + } + + runner = [&](const Platform::Path &output) { + SS.exportChordTol = chordTol; + + SS.ExportViewOrWireframeTo(output, /*exportWireframe=*/true); + }; + } else if(args[1] == "export-mesh") { + for(size_t argn = 2; argn < args.size(); argn++) { + if(!(ParseInputFile(argn) || + ParseOutputPattern(argn) || + ParseChordTolerance(argn))) { + fprintf(stderr, "Unrecognized option '%s'.\n", args[argn].c_str()); + return false; + } + } + + runner = [&](const Platform::Path &output) { + SS.exportChordTol = chordTol; + + SS.ExportMeshTo(output); + }; + } else if(args[1] == "export-surfaces") { + for(size_t argn = 2; argn < args.size(); argn++) { + if(!(ParseInputFile(argn) || + ParseOutputPattern(argn))) { + fprintf(stderr, "Unrecognized option '%s'.\n", args[argn].c_str()); + return false; + } + } + + runner = [&](const Platform::Path &output) { + StepFileWriter sfw = {}; + sfw.ExportSurfacesTo(output); + }; + } else if(args[1] == "regenerate") { + for(size_t argn = 2; argn < args.size(); argn++) { + if(!(ParseInputFile(argn) || + ParseChordTolerance(argn))) { + fprintf(stderr, "Unrecognized option '%s'.\n", args[argn].c_str()); + return false; + } + } + + outputPattern = "%.slvs"; + + runner = [&](const Platform::Path &output) { + SS.exportChordTol = chordTol; + SS.exportMode = true; + + SS.SaveToFile(output); + }; + } else { + fprintf(stderr, "Unrecognized command '%s'.\n", args[1].c_str()); + return false; + } + + if(outputPattern.empty()) { + fprintf(stderr, "An output pattern must be specified.\n"); + return false; + } else if(outputPattern.find('%') == std::string::npos && inputFiles.size() > 1) { + fprintf(stderr, + "Output pattern must include a %% symbol when using multiple inputs!\n"); + return false; + } + + if(inputFiles.empty()) { + fprintf(stderr, "At least one input file must be specified.\n"); + return false; + } + + for(const Platform::Path &inputFile : inputFiles) { + Platform::Path absInputFile = inputFile.Expand(/*fromCurrentDirectory=*/true); + + Platform::Path outputFile = Platform::Path::From(outputPattern); + size_t replaceAt = outputFile.raw.find('%'); + if(replaceAt != std::string::npos) { + Platform::Path outputSubst = inputFile.Parent(); + if(outputSubst.IsEmpty()) { + outputSubst = Platform::Path::From(inputFile.FileStem()); + } else { + outputSubst = outputSubst.Join(inputFile.FileStem()); + } + outputFile.raw.replace(replaceAt, 1, outputSubst.raw); + } + Platform::Path absOutputFile = outputFile.Expand(/*fromCurrentDirectory=*/true); + + SS.Init(); + if(!SS.LoadFromFile(absInputFile)) { + fprintf(stderr, "Cannot load '%s'!\n", inputFile.raw.c_str()); + return false; + } + SS.AfterNewFile(); + runner(absOutputFile); + SK.Clear(); + SS.Clear(); + + fprintf(stderr, "Written '%s'.\n", outputFile.raw.c_str()); + } + + return true; +} + +int main(int argc, char **argv) { + std::vector args = Platform::InitCli(argc, argv); + + if(args.size() == 1) { + ShowUsage(args[0]); + return 0; + } + + if(!RunCommand(args)) { + return 1; + } else { + return 0; + } +} diff --git a/src/platform/entrygui.cpp b/src/platform/entrygui.cpp new file mode 100644 index 0000000..397831a --- /dev/null +++ b/src/platform/entrygui.cpp @@ -0,0 +1,42 @@ +//----------------------------------------------------------------------------- +// Our main() function for the graphical interface. +// +// Copyright 2018 +//----------------------------------------------------------------------------- +#include "solvespace.h" +#if defined(WIN32) +# include +#endif + +using namespace SolveSpace; + +int main(int argc, char** argv) { + std::vector args = Platform::InitGui(argc, argv); + + Platform::Open3DConnexion(); + SS.Init(); + + if(args.size() >= 2) { + if(args.size() > 2) { + dbp("Only the first file passed on command line will be opened."); + } + + SS.Load(Platform::Path::From(args.back())); + } + + Platform::RunGui(); + + Platform::Close3DConnexion(); + SS.Clear(); + SK.Clear(); + Platform::ClearGui(); + + return 0; +} + +#if defined(WIN32) +int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, + LPSTR lpCmdLine, INT nCmdShow) { + return main(0, NULL); +} +#endif diff --git a/src/platform/gui.cpp b/src/platform/gui.cpp new file mode 100644 index 0000000..0c9637f --- /dev/null +++ b/src/platform/gui.cpp @@ -0,0 +1,136 @@ +//----------------------------------------------------------------------------- +// Platform-dependent GUI functionality that has only minor differences. +// +// Copyright 2018 whitequark +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { +namespace Platform { + +//----------------------------------------------------------------------------- +// Keyboard events +//----------------------------------------------------------------------------- + +std::string AcceleratorDescription(const KeyboardEvent &accel) { + std::string label; + if(accel.controlDown) { +#ifdef __APPLE__ + label += "⌘+"; +#else + label += "Ctrl+"; +#endif + } + + if(accel.shiftDown) { + label += "Shift+"; + } + + switch(accel.key) { + case KeyboardEvent::Key::FUNCTION: + label += ssprintf("F%d", accel.num); + break; + + case KeyboardEvent::Key::CHARACTER: + if(accel.chr == '\t') { + label += "Tab"; + } else if(accel.chr == ' ') { + label += "Space"; + } else if(accel.chr == '\x1b') { + label += "Esc"; + } else if(accel.chr == '\x7f') { + label += "Del"; + } else if(accel.chr != 0) { + label += toupper((char)(accel.chr & 0xff)); + } + break; + } + + return label; +} + +//----------------------------------------------------------------------------- +// Settings +//----------------------------------------------------------------------------- + +void Settings::FreezeBool(const std::string &key, bool value) { + FreezeInt(key, (int)value); +} + +bool Settings::ThawBool(const std::string &key, bool defaultValue) { + return ThawInt(key, (int)defaultValue) != 0; +} + +void Settings::FreezeColor(const std::string &key, RgbaColor value) { + FreezeInt(key, value.ToPackedInt()); +} + +RgbaColor Settings::ThawColor(const std::string &key, RgbaColor defaultValue) { + return RgbaColor::FromPackedInt(ThawInt(key, defaultValue.ToPackedInt())); +} + +//----------------------------------------------------------------------------- +// File dialogs +//----------------------------------------------------------------------------- + +void FileDialog::AddFilter(const FileFilter &filter) { + AddFilter(Translate("file-type", filter.name.c_str()), filter.extensions); +} + +void FileDialog::AddFilters(const std::vector &filters) { + for(auto filter : filters) AddFilter(filter); +} + +std::vector SolveSpaceModelFileFilters = { + { CN_("file-type", "SolveSpace models"), { "slvs" } }, +}; + +std::vector SolveSpaceLinkFileFilters = { + { CN_("file-type", "ALL"), { "slvs", "emn", "stl" } }, + { CN_("file-type", "SolveSpace models"), { "slvs" } }, + { CN_("file-type", "IDF circuit board"), { "emn" } }, + { CN_("file-type", "STL triangle mesh"), { "stl" } }, +}; + +std::vector RasterFileFilters = { + { CN_("file-type", "PNG image"), { "png" } }, +}; + +std::vector MeshFileFilters = { + { CN_("file-type", "STL mesh"), { "stl" } }, + { CN_("file-type", "Wavefront OBJ mesh"), { "obj" } }, + { CN_("file-type", "Three.js-compatible mesh, with viewer"), { "html" } }, + { CN_("file-type", "Three.js-compatible mesh, mesh only"), { "js" } }, + { CN_("file-type", "VRML text file"), { "wrl" } }, +}; + +std::vector SurfaceFileFilters = { + { CN_("file-type", "STEP file"), { "step", "stp" } }, +}; + +std::vector VectorFileFilters = { + { CN_("file-type", "PDF file"), { "pdf" } }, + { CN_("file-type", "Encapsulated PostScript"), { "eps", "ps" } }, + { CN_("file-type", "Scalable Vector Graphics"), { "svg" } }, + { CN_("file-type", "STEP file"), { "step", "stp" } }, + { CN_("file-type", "DXF file (AutoCAD 2007)"), { "dxf" } }, + { CN_("file-type", "HPGL file"), { "plt", "hpgl" } }, + { CN_("file-type", "G Code"), { "ngc", "txt" } }, +}; + +std::vector Vector3dFileFilters = { + { CN_("file-type", "STEP file"), { "step", "stp" } }, + { CN_("file-type", "DXF file (AutoCAD 2007)"), { "dxf" } }, +}; + +std::vector ImportFileFilters = { + { CN_("file-type", "AutoCAD DXF and DWG files"), { "dxf", "dwg" } }, +}; + +std::vector CsvFileFilters = { + { CN_("file-type", "Comma-separated values"), { "csv" } }, +}; + + +} +} diff --git a/src/platform/gui.h b/src/platform/gui.h new file mode 100644 index 0000000..46dcc18 --- /dev/null +++ b/src/platform/gui.h @@ -0,0 +1,400 @@ +//----------------------------------------------------------------------------- +// An abstraction for platform-dependent GUI functionality. +// +// Copyright 2018 whitequark +//----------------------------------------------------------------------------- + +#ifndef SOLVESPACE_GUI_H +#define SOLVESPACE_GUI_H + +#include +#include +#include +#include + +#include "platform.h" + +namespace SolveSpace { +class RgbaColor; + +namespace Platform { + +//----------------------------------------------------------------------------- +// Events +//----------------------------------------------------------------------------- + +// A mouse input event. +class MouseEvent { +public: + enum class Type { + MOTION, + PRESS, + DBL_PRESS, + RELEASE, + SCROLL_VERT, + LEAVE, + }; + + enum class Button { + NONE, + LEFT, + MIDDLE, + RIGHT, + }; + + Type type; + double x; + double y; + bool shiftDown; + bool controlDown; + union { + Button button; // for Type::{MOTION,PRESS,DBL_PRESS,RELEASE} + double scrollDelta; // for Type::SCROLL_VERT + }; +}; + +// A 3-DOF input event. +struct SixDofEvent { + enum class Type { + MOTION, + PRESS, + RELEASE, + }; + + enum class Button { + FIT, + }; + + Type type; + bool shiftDown; + bool controlDown; + double translationX, translationY, translationZ; // for Type::MOTION + double rotationX, rotationY, rotationZ; // for Type::MOTION + Button button; // for Type::{PRESS,RELEASE} +}; + +// A keyboard input event. +struct KeyboardEvent { + enum class Type { + PRESS, + RELEASE, + }; + + enum class Key { + CHARACTER, + FUNCTION, + }; + + Type type; + Key key; + union { + char32_t chr; // for Key::CHARACTER + int num; // for Key::FUNCTION + }; + bool shiftDown; + bool controlDown; + + bool Equals(const KeyboardEvent &other) { + return type == other.type && key == other.key && + shiftDown == other.shiftDown && controlDown == other.controlDown && + ((key == Key::CHARACTER && chr == other.chr) || + (key == Key::FUNCTION && num == other.num)); + } +}; + +std::string AcceleratorDescription(const KeyboardEvent &accel); + +//----------------------------------------------------------------------------- +// Interfaces +//----------------------------------------------------------------------------- + +// Handling fatal errors. +[[noreturn]] +void FatalError(const std::string &message); + +// A native settings store. +class Settings { +public: + virtual ~Settings() = default; + + virtual void FreezeInt(const std::string &key, uint32_t value) = 0; + virtual uint32_t ThawInt(const std::string &key, uint32_t defaultValue = 0) = 0; + + virtual void FreezeFloat(const std::string &key, double value) = 0; + virtual double ThawFloat(const std::string &key, double defaultValue = 0.0) = 0; + + virtual void FreezeString(const std::string &key, const std::string &value) = 0; + virtual std::string ThawString(const std::string &key, + const std::string &defaultValue = "") = 0; + + virtual void FreezeBool(const std::string &key, bool value); + virtual bool ThawBool(const std::string &key, bool defaultValue = false); + + virtual void FreezeColor(const std::string &key, RgbaColor value); + virtual RgbaColor ThawColor(const std::string &key, RgbaColor defaultValue); +}; + +typedef std::shared_ptr SettingsRef; + +SettingsRef GetSettings(); + +// A native single-shot timer. +class Timer { +public: + std::function onTimeout; + + virtual ~Timer() = default; + + virtual void RunAfter(unsigned milliseconds) = 0; + virtual void RunAfterNextFrame() { RunAfter(1); } + virtual void RunAfterProcessingEvents() { RunAfter(0); } +}; + +typedef std::shared_ptr TimerRef; + +TimerRef CreateTimer(); + +// A native menu item. +class MenuItem { +public: + enum class Indicator { + NONE, + CHECK_MARK, + RADIO_MARK, + }; + + std::function onTrigger; + + virtual ~MenuItem() = default; + + virtual void SetAccelerator(KeyboardEvent accel) = 0; + virtual void SetIndicator(Indicator type) = 0; + virtual void SetEnabled(bool enabled) = 0; + virtual void SetActive(bool active) = 0; +}; + +typedef std::shared_ptr MenuItemRef; + +// A native menu. +class Menu { +public: + virtual ~Menu() = default; + + virtual std::shared_ptr AddItem( + const std::string &label, std::function onTrigger = std::function(), + bool mnemonics = true) = 0; + virtual std::shared_ptr
AddSubMenu(const std::string &label) = 0; + virtual void AddSeparator() = 0; + + virtual void PopUp() = 0; + + virtual void Clear() = 0; +}; + +typedef std::shared_ptr MenuRef; + +// A native menu bar. +class MenuBar { +public: + virtual ~MenuBar() = default; + + virtual std::shared_ptr AddSubMenu(const std::string &label) = 0; + + virtual void Clear() = 0; +}; + +typedef std::shared_ptr MenuBarRef; + +MenuRef CreateMenu(); +MenuBarRef GetOrCreateMainMenu(bool *unique); + +// A native top-level window, with an OpenGL context, and an editor overlay. +class Window { +public: + enum class Kind { + TOPLEVEL, + TOOL, + }; + + enum class Cursor { + POINTER, + HAND + }; + + std::function onClose; + std::function onFullScreen; + std::function onMouseEvent; + std::function onSixDofEvent; + std::function onKeyboardEvent; + std::function onEditingDone; + std::function onScrollbarAdjusted; + std::function onContextLost; + std::function onRender; + + virtual ~Window() = default; + + // Returns physical display DPI. + virtual double GetPixelDensity() = 0; + // Returns raster graphics and coordinate scale (already applied on the platform side), + // i.e. size of logical pixel in physical pixels, or device pixel ratio. + virtual double GetDevicePixelRatio() = 0; + // Returns (fractional) font scale, to be applied on top of (integral) device pixel ratio. + virtual double GetDeviceFontScale() { + return GetPixelDensity() / GetDevicePixelRatio() / 96.0; + } + + virtual bool IsVisible() = 0; + virtual void SetVisible(bool visible) = 0; + virtual void Focus() = 0; + + virtual bool IsFullScreen() = 0; + virtual void SetFullScreen(bool fullScreen) = 0; + + virtual void SetTitle(const std::string &title) = 0; + virtual bool SetTitleForFilename(const Path &filename) { return false; } + + virtual void SetMenuBar(MenuBarRef menuBar) = 0; + + virtual void GetContentSize(double *width, double *height) = 0; + virtual void SetMinContentSize(double width, double height) = 0; + + virtual void FreezePosition(SettingsRef settings, const std::string &key) = 0; + virtual void ThawPosition(SettingsRef settings, const std::string &key) = 0; + + virtual void SetCursor(Cursor cursor) = 0; + virtual void SetTooltip(const std::string &text, double x, double y, + double width, double height) = 0; + + virtual bool IsEditorVisible() = 0; + virtual void ShowEditor(double x, double y, double fontHeight, double minWidth, + bool isMonospace, const std::string &text) = 0; + virtual void HideEditor() = 0; + + virtual void SetScrollbarVisible(bool visible) = 0; + virtual void ConfigureScrollbar(double min, double max, double pageSize) = 0; + virtual double GetScrollbarPosition() = 0; + virtual void SetScrollbarPosition(double pos) = 0; + + virtual void Invalidate() = 0; +}; + +typedef std::shared_ptr WindowRef; + +WindowRef CreateWindow(Window::Kind kind = Window::Kind::TOPLEVEL, + WindowRef parentWindow = NULL); + +// 3DConnexion support. +void Open3DConnexion(); +void Close3DConnexion(); +void Request3DConnexionEventsForWindow(WindowRef window); + +// A native dialog that asks for one choice out of several. +class MessageDialog { +public: + enum class Type { + INFORMATION, + QUESTION, + WARNING, + ERROR + }; + + enum class Response { + NONE, + OK, + YES, + NO, + CANCEL + }; + + std::function onResponse; + + virtual ~MessageDialog() = default; + + virtual void SetType(Type type) = 0; + virtual void SetTitle(std::string title) = 0; + virtual void SetMessage(std::string message) = 0; + virtual void SetDescription(std::string description) = 0; + + virtual void AddButton(std::string label, Response response, bool isDefault = false) = 0; + + virtual Response RunModal() = 0; + virtual void ShowModal() { + Response response = RunModal(); + if(onResponse) { + onResponse(response); + } + } +}; + +typedef std::shared_ptr MessageDialogRef; + +MessageDialogRef CreateMessageDialog(WindowRef parentWindow); + +// A file filter. +struct FileFilter { + std::string name; + std::vector extensions; +}; + +// SolveSpace's native file format +extern std::vector SolveSpaceModelFileFilters; +// SolveSpace's linkable file formats +extern std::vector SolveSpaceLinkFileFilters; +// Raster image +extern std::vector RasterFileFilters; +// Triangle mesh +extern std::vector MeshFileFilters; +// NURBS surfaces +extern std::vector SurfaceFileFilters; +// 2d vector (lines and curves) format +extern std::vector VectorFileFilters; +// 3d vector (wireframe lines and curves) format +extern std::vector Vector3dFileFilters; +// Any importable format +extern std::vector ImportFileFilters; +// Comma-separated value, like a spreadsheet would use +extern std::vector CsvFileFilters; + +// A native dialog that asks to choose a file. +class FileDialog { +public: + virtual ~FileDialog() = default; + + virtual void SetTitle(std::string title) = 0; + virtual void SetCurrentName(std::string name) = 0; + + virtual Platform::Path GetFilename() = 0; + virtual void SetFilename(Platform::Path path) = 0; + virtual void SuggestFilename(Platform::Path path) = 0; + + virtual void AddFilter(std::string name, std::vector extensions) = 0; + void AddFilter(const FileFilter &filter); + void AddFilters(const std::vector &filters); + + virtual void FreezeChoices(SettingsRef settings, const std::string &key) = 0; + virtual void ThawChoices(SettingsRef settings, const std::string &key) = 0; + + virtual bool RunModal() = 0; +}; + +typedef std::shared_ptr FileDialogRef; + +FileDialogRef CreateOpenFileDialog(WindowRef parentWindow); +FileDialogRef CreateSaveFileDialog(WindowRef parentWindow); + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +std::vector GetFontFiles(); +void OpenInBrowser(const std::string &url); + +std::vector InitGui(int argc, char **argv); +void RunGui(); +void ExitGui(); +void ClearGui(); + +} +} // namespace SolveSpace + +#endif diff --git a/src/platform/guigtk.cpp b/src/platform/guigtk.cpp new file mode 100644 index 0000000..522fb13 --- /dev/null +++ b/src/platform/guigtk.cpp @@ -0,0 +1,1679 @@ +//----------------------------------------------------------------------------- +// The GTK-based implementation of platform-dependent GUI functionality. +// +// Copyright 2018 whitequark +//----------------------------------------------------------------------------- +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#if defined(HAVE_GTK_FILECHOOSERNATIVE) +# include +#endif + +#if defined(HAVE_SPACEWARE) +# include +# include +# if defined(GDK_WINDOWING_X11) +# include +# endif +# if defined(GDK_WINDOWING_WAYLAND) +# include +# endif +# if GTK_CHECK_VERSION(3, 20, 0) +# include +# else +# include +# endif +#endif + +#include "solvespace.h" + +namespace SolveSpace { +namespace Platform { + +//----------------------------------------------------------------------------- +// Utility functions +//----------------------------------------------------------------------------- + +static std::string PrepareMnemonics(std::string label) { + std::replace(label.begin(), label.end(), '&', '_'); + return label; +} + +static std::string PrepareTitle(const std::string &title) { + return title + " — SolveSpace"; +} + +//----------------------------------------------------------------------------- +// Fatal errors +//----------------------------------------------------------------------------- + +void FatalError(const std::string &message) { + fprintf(stderr, "%s", message.c_str()); + abort(); +} + +//----------------------------------------------------------------------------- +// Settings +//----------------------------------------------------------------------------- + +class SettingsImplGtk final : public Settings { +public: + // Why aren't we using GSettings? Two reasons. It doesn't allow to easily see whether + // the setting had the default value, and it requires to install a schema globally. + Path _path; + json_object *_json = NULL; + + static Path GetConfigPath() { + Path configHome; + if(getenv("XDG_CONFIG_HOME")) { + configHome = Path::From(getenv("XDG_CONFIG_HOME")); + } else if(getenv("HOME")) { + configHome = Path::From(getenv("HOME")).Join(".config"); + } else { + dbp("neither XDG_CONFIG_HOME nor HOME are set"); + return Path::From(""); + } + if(!configHome.IsEmpty()) { + configHome = configHome.Join("solvespace"); + } + + const char *configHomeC = configHome.raw.c_str(); + struct stat st; + if(stat(configHomeC, &st)) { + if(errno == ENOENT) { + if(mkdir(configHomeC, 0777)) { + dbp("cannot mkdir %s: %s", configHomeC, strerror(errno)); + return Path::From(""); + } + } else { + dbp("cannot stat %s: %s", configHomeC, strerror(errno)); + return Path::From(""); + } + } else if(!S_ISDIR(st.st_mode)) { + dbp("%s is not a directory", configHomeC); + return Path::From(""); + } + + return configHome.Join("settings.json"); + } + + SettingsImplGtk() { + _path = GetConfigPath(); + if(_path.IsEmpty()) { + dbp("settings will not be saved"); + } else { + _json = json_object_from_file(_path.raw.c_str()); + if(!_json && errno != ENOENT) { + dbp("cannot load settings: %s", strerror(errno)); + } + } + + if(_json == NULL) { + _json = json_object_new_object(); + } + } + + ~SettingsImplGtk() override { + if(!_path.IsEmpty()) { + // json-c <0.12 has the first argument non-const + if(json_object_to_file_ext((char *)_path.raw.c_str(), _json, + JSON_C_TO_STRING_PRETTY)) { + dbp("cannot save settings: %s", strerror(errno)); + } + } + + json_object_put(_json); + } + + void FreezeInt(const std::string &key, uint32_t value) override { + struct json_object *jsonValue = json_object_new_int(value); + json_object_object_add(_json, key.c_str(), jsonValue); + } + + uint32_t ThawInt(const std::string &key, uint32_t defaultValue) override { + struct json_object *jsonValue; + if(json_object_object_get_ex(_json, key.c_str(), &jsonValue)) { + return json_object_get_int(jsonValue); + } + return defaultValue; + } + + void FreezeBool(const std::string &key, bool value) override { + struct json_object *jsonValue = json_object_new_boolean(value); + json_object_object_add(_json, key.c_str(), jsonValue); + } + + bool ThawBool(const std::string &key, bool defaultValue) override { + struct json_object *jsonValue; + if(json_object_object_get_ex(_json, key.c_str(), &jsonValue)) { + return json_object_get_boolean(jsonValue); + } + return defaultValue; + } + + void FreezeFloat(const std::string &key, double value) override { + struct json_object *jsonValue = json_object_new_double(value); + json_object_object_add(_json, key.c_str(), jsonValue); + } + + double ThawFloat(const std::string &key, double defaultValue) override { + struct json_object *jsonValue; + if(json_object_object_get_ex(_json, key.c_str(), &jsonValue)) { + return json_object_get_double(jsonValue); + } + return defaultValue; + } + + void FreezeString(const std::string &key, const std::string &value) override { + struct json_object *jsonValue = json_object_new_string(value.c_str()); + json_object_object_add(_json, key.c_str(), jsonValue); + } + + std::string ThawString(const std::string &key, + const std::string &defaultValue = "") override { + struct json_object *jsonValue; + if(json_object_object_get_ex(_json, key.c_str(), &jsonValue)) { + return json_object_get_string(jsonValue); + } + return defaultValue; + } +}; + +SettingsRef GetSettings() { + static std::shared_ptr settings; + if(!settings) { + settings = std::make_shared(); + } + return settings; +} + +//----------------------------------------------------------------------------- +// Timers +//----------------------------------------------------------------------------- + +class TimerImplGtk final : public Timer { +public: + sigc::connection _connection; + + void RunAfter(unsigned milliseconds) override { + if(!_connection.empty()) { + _connection.disconnect(); + } + + auto handler = [this]() { + if(this->onTimeout) { + this->onTimeout(); + } + return false; + }; + // Note: asan warnings about new-delete-type-mismatch are false positives here: + // https://gitlab.gnome.org/GNOME/gtkmm/-/issues/65 + // Pass new_delete_type_mismatch=0 to ASAN_OPTIONS to disable those warnings. + // Unfortunately they won't go away until upgrading to gtkmm4 + _connection = Glib::signal_timeout().connect(handler, milliseconds); + } +}; + +TimerRef CreateTimer() { + return std::make_shared(); +} + +//----------------------------------------------------------------------------- +// GTK menu extensions +//----------------------------------------------------------------------------- + +class GtkMenuItem : public Gtk::CheckMenuItem { + Platform::MenuItem *_receiver; + bool _has_indicator; + bool _synthetic_event; + +public: + GtkMenuItem(Platform::MenuItem *receiver) : + _receiver(receiver), _has_indicator(false), _synthetic_event(false) { + } + + void set_accel_key(const Gtk::AccelKey &accel_key) { + Gtk::CheckMenuItem::set_accel_key(accel_key); + } + + bool has_indicator() const { + return _has_indicator; + } + + void set_has_indicator(bool has_indicator) { + _has_indicator = has_indicator; + } + + void set_active(bool active) { + if(Gtk::CheckMenuItem::get_active() == active) + return; + + _synthetic_event = true; + Gtk::CheckMenuItem::set_active(active); + _synthetic_event = false; + } + +protected: + void on_activate() override { + Gtk::CheckMenuItem::on_activate(); + + if(!_synthetic_event && _receiver->onTrigger) { + _receiver->onTrigger(); + } + } + + void draw_indicator_vfunc(const Cairo::RefPtr &cr) override { + if(_has_indicator) { + Gtk::CheckMenuItem::draw_indicator_vfunc(cr); + } + } +}; + +//----------------------------------------------------------------------------- +// Menus +//----------------------------------------------------------------------------- + +class MenuItemImplGtk final : public MenuItem { +public: + GtkMenuItem gtkMenuItem; + + MenuItemImplGtk() : gtkMenuItem(this) {} + + void SetAccelerator(KeyboardEvent accel) override { + guint accelKey = 0; + if(accel.key == KeyboardEvent::Key::CHARACTER) { + if(accel.chr == '\t') { + accelKey = GDK_KEY_Tab; + } else if(accel.chr == '\x1b') { + accelKey = GDK_KEY_Escape; + } else if(accel.chr == '\x7f') { + accelKey = GDK_KEY_Delete; + } else { + accelKey = gdk_unicode_to_keyval(accel.chr); + } + } else if(accel.key == KeyboardEvent::Key::FUNCTION) { + accelKey = GDK_KEY_F1 + accel.num - 1; + } + + Gdk::ModifierType accelMods = {}; + if(accel.shiftDown) { + accelMods |= Gdk::SHIFT_MASK; + } + if(accel.controlDown) { + accelMods |= Gdk::CONTROL_MASK; + } + + gtkMenuItem.set_accel_key(Gtk::AccelKey(accelKey, accelMods)); + } + + void SetIndicator(Indicator type) override { + switch(type) { + case Indicator::NONE: + gtkMenuItem.set_has_indicator(false); + break; + + case Indicator::CHECK_MARK: + gtkMenuItem.set_has_indicator(true); + gtkMenuItem.set_draw_as_radio(false); + break; + + case Indicator::RADIO_MARK: + gtkMenuItem.set_has_indicator(true); + gtkMenuItem.set_draw_as_radio(true); + break; + } + } + + void SetActive(bool active) override { + ssassert(gtkMenuItem.has_indicator(), + "Cannot change state of a menu item without indicator"); + gtkMenuItem.set_active(active); + } + + void SetEnabled(bool enabled) override { + gtkMenuItem.set_sensitive(enabled); + } +}; + +class MenuImplGtk final : public Menu { +public: + Gtk::Menu gtkMenu; + std::vector> menuItems; + std::vector> subMenus; + + MenuItemRef AddItem(const std::string &label, + std::function onTrigger = NULL, + bool mnemonics = true) override { + auto menuItem = std::make_shared(); + menuItems.push_back(menuItem); + + menuItem->gtkMenuItem.set_label(mnemonics ? PrepareMnemonics(label) : label); + menuItem->gtkMenuItem.set_use_underline(mnemonics); + menuItem->gtkMenuItem.show(); + menuItem->onTrigger = onTrigger; + gtkMenu.append(menuItem->gtkMenuItem); + + return menuItem; + } + + MenuRef AddSubMenu(const std::string &label) override { + auto menuItem = std::make_shared(); + menuItems.push_back(menuItem); + + auto subMenu = std::make_shared(); + subMenus.push_back(subMenu); + + menuItem->gtkMenuItem.set_label(PrepareMnemonics(label)); + menuItem->gtkMenuItem.set_use_underline(true); + menuItem->gtkMenuItem.set_submenu(subMenu->gtkMenu); + menuItem->gtkMenuItem.show_all(); + gtkMenu.append(menuItem->gtkMenuItem); + + return subMenu; + } + + void AddSeparator() override { + Gtk::SeparatorMenuItem *gtkMenuItem = Gtk::manage(new Gtk::SeparatorMenuItem()); + gtkMenuItem->show(); + gtkMenu.append(*Gtk::manage(gtkMenuItem)); + } + + void PopUp() override { + Glib::RefPtr loop = Glib::MainLoop::create(); + auto signal = gtkMenu.signal_deactivate().connect([&]() { loop->quit(); }); + + gtkMenu.show_all(); + gtkMenu.popup(0, GDK_CURRENT_TIME); + loop->run(); + signal.disconnect(); + } + + void Clear() override { + gtkMenu.foreach([&](Gtk::Widget &w) { gtkMenu.remove(w); }); + menuItems.clear(); + subMenus.clear(); + } +}; + +MenuRef CreateMenu() { + return std::make_shared(); +} + +class MenuBarImplGtk final : public MenuBar { +public: + Gtk::MenuBar gtkMenuBar; + std::vector> subMenus; + + MenuRef AddSubMenu(const std::string &label) override { + auto subMenu = std::make_shared(); + subMenus.push_back(subMenu); + + Gtk::MenuItem *gtkMenuItem = Gtk::manage(new Gtk::MenuItem); + gtkMenuItem->set_label(PrepareMnemonics(label)); + gtkMenuItem->set_use_underline(true); + gtkMenuItem->set_submenu(subMenu->gtkMenu); + gtkMenuItem->show_all(); + gtkMenuBar.append(*gtkMenuItem); + + return subMenu; + } + + void Clear() override { + gtkMenuBar.foreach([&](Gtk::Widget &w) { gtkMenuBar.remove(w); }); + subMenus.clear(); + } +}; + +MenuBarRef GetOrCreateMainMenu(bool *unique) { + *unique = false; + return std::make_shared(); +} + +//----------------------------------------------------------------------------- +// GTK GL and window extensions +//----------------------------------------------------------------------------- + +class GtkGLWidget : public Gtk::GLArea { + struct LastPress { + double x, y; + guint button; + guint32 time; + GdkDevice *device; + }; + + Window *_receiver; + LastPress _last_press; + +public: + GtkGLWidget(Platform::Window *receiver) : _receiver(receiver), _last_press() { + set_has_depth_buffer(true); + set_can_focus(true); + set_events(Gdk::POINTER_MOTION_MASK | + Gdk::BUTTON_PRESS_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::BUTTON_MOTION_MASK | + Gdk::SCROLL_MASK | + Gdk::SMOOTH_SCROLL_MASK | + Gdk::TOUCHPAD_GESTURE_MASK | + Gdk::LEAVE_NOTIFY_MASK | + Gdk::KEY_PRESS_MASK | + Gdk::KEY_RELEASE_MASK); + } + +protected: + // Work around a bug fixed in GTKMM 3.22: + // https://mail.gnome.org/archives/gtkmm-list/2016-April/msg00020.html + Glib::RefPtr on_create_context() override { + return get_window()->create_gl_context(); + } + + bool on_render(const Glib::RefPtr &context) override { + if(_receiver->onRender) { + _receiver->onRender(); + } + return true; + } + + bool process_pointer_event(MouseEvent::Type type, double x, double y, + guint state, guint button = 0, double scroll_delta = 0) { + MouseEvent event = {}; + event.type = type; + event.x = x; + event.y = y; + if(button == 1 || (state & GDK_BUTTON1_MASK) != 0) { + event.button = MouseEvent::Button::LEFT; + } else if(button == 2 || (state & GDK_BUTTON2_MASK) != 0) { + event.button = MouseEvent::Button::MIDDLE; + } else if(button == 3 || (state & GDK_BUTTON3_MASK) != 0) { + event.button = MouseEvent::Button::RIGHT; + } + if((state & GDK_SHIFT_MASK) != 0) { + event.shiftDown = true; + } + if((state & GDK_CONTROL_MASK) != 0) { + event.controlDown = true; + } + if(scroll_delta != 0) { + event.scrollDelta = scroll_delta; + } + + if(_receiver->onMouseEvent) { + return _receiver->onMouseEvent(event); + } + + return false; + } + + bool on_motion_notify_event(GdkEventMotion *gdk_event) override { + double x,y; + GdkModifierType state; + gdk_event_get_coords((GdkEvent*)gdk_event, &x, &y); + gdk_event_get_state((GdkEvent*)gdk_event, &state); + + if(process_pointer_event(MouseEvent::Type::MOTION, x, y, state)) + return true; + + return Gtk::GLArea::on_motion_notify_event(gdk_event); + } + + bool on_button_press_event(GdkEventButton *gdk_event) override { + MouseEvent::Type type; + GdkEventType gdk_type; + gdk_type = gdk_event_get_event_type((GdkEvent*)gdk_event); + + if(gdk_type == GDK_BUTTON_PRESS) { + type = MouseEvent::Type::PRESS; + } else if(gdk_type == GDK_2BUTTON_PRESS) { + type = MouseEvent::Type::DBL_PRESS; + } else { + return Gtk::GLArea::on_button_press_event(gdk_event); + } + double x,y; + gdk_event_get_coords((GdkEvent*)gdk_event, &x, &y); + GdkModifierType state; + gdk_event_get_state((GdkEvent*)gdk_event, &state); + guint button; + gdk_event_get_button((GdkEvent*)gdk_event, &button); + + // In GTK, the sequence of events for a double click is: + // - press + // - release + // - press + // - double-press + // - release + // + // Having a press event right before the double press event is inconsistent with + // the way double click handling works on Windows and macOS, and may cause receiver + // code to treat the sequence as three press events. + // To avoid this, we check if a press event happened quickly enough after the + // previous one to be considered a double press, and eat it up if a receiver + // is set up. + // Since we ignore triple press events, we reset the last press data when receiving + // the double press event, in order to avoid eating up the third press event of + // a triple click sequence. + if(gdk_type == GDK_BUTTON_PRESS) { + LastPress c = { + x, y, button, + gdk_event_get_time((GdkEvent*)gdk_event), + gdk_event_get_device((GdkEvent*)gdk_event), + }; + LastPress p = _last_press; + _last_press = c; + + if(_receiver->onMouseEvent) { + const Glib::RefPtr settings = get_settings(); + const guint dbl_press_time = settings-> + property_gtk_double_click_time().get_value(); + const double dbl_press_distance = settings-> + property_gtk_double_click_distance().get_value(); + + if(c.device == p.device && + c.button == p.button && + c.time - p.time < dbl_press_time && + std::abs(c.x - p.x) <= dbl_press_distance && + std::abs(c.y - p.y) <= dbl_press_distance) { + // Eat this press event, as it'll be followed immediately + // by a double press event + return true; + } + } + } else if(gdk_type == GDK_2BUTTON_PRESS) { + const guint32 time = gdk_event_get_time((GdkEvent*)gdk_event); + const GdkDevice *device = gdk_event_get_device((GdkEvent*)gdk_event); + // A double press event is synthesised by GTK, and has the exact same + // time and device as the press event the preceeded it. + ssassert(_last_press.time == time && _last_press.device == device, + "double press event not following a press event"); + // Reset in order to avoid eating the press event for a triple click + _last_press = {}; + } + + if(process_pointer_event(type, x, y, state, button)) + return true; + + return Gtk::GLArea::on_button_press_event(gdk_event); + } + + bool on_button_release_event(GdkEventButton *gdk_event) override { + double x,y; + gdk_event_get_coords((GdkEvent*)gdk_event, &x, &y); + GdkModifierType state; + gdk_event_get_state((GdkEvent*)gdk_event, &state); + guint button; + gdk_event_get_button((GdkEvent*)gdk_event, &button); + if(process_pointer_event(MouseEvent::Type::RELEASE, x, y, state, button)) + return true; + + return Gtk::GLArea::on_button_release_event(gdk_event); + } + + bool on_scroll_event(GdkEventScroll *gdk_event) override { + double dx, dy; + GdkScrollDirection dir; + double delta; + +// for gtk4 ?? +// gdk_scroll_event_get_deltas((GdkEvent*)gdk_event, &dx, &dy); +// gdk_scroll_event_get_direction((GdkEvent*)gdk_event, &dir); + + if(gdk_event_get_scroll_deltas((GdkEvent*)gdk_event, &dx, &dy)) { + delta = -dy; + } else if(gdk_event_get_scroll_direction((GdkEvent*)gdk_event, &dir)) { + if(dir == GDK_SCROLL_UP) { + delta = 1; + } else if(dir == GDK_SCROLL_DOWN) { + delta = -1; + } else { + return false; + } + } else { + return false; + } + + double x,y; + gdk_event_get_coords((GdkEvent*)gdk_event, &x, &y); + GdkModifierType state; + gdk_event_get_state((GdkEvent*)gdk_event, &state); + + if(process_pointer_event(MouseEvent::Type::SCROLL_VERT, + x, y, state, 0, delta)) + return true; + + return Gtk::GLArea::on_scroll_event(gdk_event); + } + + bool on_leave_notify_event(GdkEventCrossing *gdk_event) override { + double x,y; + gdk_event_get_coords((GdkEvent*)gdk_event, &x, &y); + GdkModifierType state; + gdk_event_get_state((GdkEvent*)gdk_event, &state); + + if(process_pointer_event(MouseEvent::Type::LEAVE, x, y, state)) + return true; + + return Gtk::GLArea::on_leave_notify_event(gdk_event); + } + + bool process_key_event(KeyboardEvent::Type type, GdkEventKey *gdk_event) { + KeyboardEvent event = {}; + event.type = type; + + GdkModifierType state; + gdk_event_get_state((GdkEvent*)gdk_event, &state); + + Gdk::ModifierType mod_mask = get_modifier_mask(Gdk::MODIFIER_INTENT_DEFAULT_MOD_MASK); + if((state & mod_mask) & ~(GDK_SHIFT_MASK|GDK_CONTROL_MASK)) { + return false; + } + + event.shiftDown = (state & GDK_SHIFT_MASK) != 0; + event.controlDown = (state & GDK_CONTROL_MASK) != 0; + + guint keyval; + gdk_event_get_keyval((GdkEvent*)gdk_event, &keyval); + + char32_t chr = gdk_keyval_to_unicode(gdk_keyval_to_lower(keyval)); + if(chr != 0) { + event.key = KeyboardEvent::Key::CHARACTER; + event.chr = chr; + } else if(keyval >= GDK_KEY_F1 && + keyval <= GDK_KEY_F12) { + event.key = KeyboardEvent::Key::FUNCTION; + event.num = keyval - GDK_KEY_F1 + 1; + } else { + return false; + } + + if(_receiver->onKeyboardEvent) { + return _receiver->onKeyboardEvent(event); + } + + return false; + } + + bool on_key_press_event(GdkEventKey *gdk_event) override { + if(process_key_event(KeyboardEvent::Type::PRESS, gdk_event)) + return true; + + return Gtk::GLArea::on_key_press_event(gdk_event); + } + + bool on_key_release_event(GdkEventKey *gdk_event) override { + if(process_key_event(KeyboardEvent::Type::RELEASE, gdk_event)) + return true; + + return Gtk::GLArea::on_key_release_event(gdk_event); + } +}; + +class GtkEditorOverlay : public Gtk::Fixed { + Window *_receiver; + GtkGLWidget _gl_widget; + Gtk::Entry _entry; + +public: + GtkEditorOverlay(Platform::Window *receiver) : _receiver(receiver), _gl_widget(receiver) { + add(_gl_widget); + + _entry.set_no_show_all(true); + _entry.set_has_frame(false); + add(_entry); + + _entry.signal_activate(). + connect(sigc::mem_fun(this, &GtkEditorOverlay::on_activate)); + } + + bool is_editing() const { + return _entry.is_visible(); + } + + void start_editing(int x, int y, int font_height, int min_width, bool is_monospace, + const std::string &val) { + Pango::FontDescription font_desc; + font_desc.set_family(is_monospace ? "monospace" : "normal"); + font_desc.set_absolute_size(font_height * Pango::SCALE); + _entry.override_font(font_desc); + + // The y coordinate denotes baseline. + Pango::FontMetrics font_metrics = get_pango_context()->get_metrics(font_desc); + y -= font_metrics.get_ascent() / Pango::SCALE; + + Glib::RefPtr layout = Pango::Layout::create(get_pango_context()); + layout->set_font_description(font_desc); + // Add one extra char width to avoid scrolling. + layout->set_text(val + " "); + int width = layout->get_logical_extents().get_width(); + + Gtk::Border margin = _entry.get_style_context()->get_margin(); + Gtk::Border border = _entry.get_style_context()->get_border(); + Gtk::Border padding = _entry.get_style_context()->get_padding(); + move(_entry, + x - margin.get_left() - border.get_left() - padding.get_left(), + y - margin.get_top() - border.get_top() - padding.get_top()); + + int fitWidth = width / Pango::SCALE + padding.get_left() + padding.get_right(); + _entry.set_size_request(max(fitWidth, min_width), -1); + queue_resize(); + + _entry.set_text(val); + + if(!_entry.is_visible()) { + _entry.show(); + _entry.grab_focus(); + + // We grab the input for ourselves and not the entry to still have + // the pointer events go through the underlay. + add_modal_grab(); + } + } + + void stop_editing() { + if(_entry.is_visible()) { + remove_modal_grab(); + _entry.hide(); + _gl_widget.grab_focus(); + } + } + + GtkGLWidget &get_gl_widget() { + return _gl_widget; + } + +protected: + bool on_key_press_event(GdkEventKey *gdk_event) override { + guint keyval; + gdk_event_get_keyval((GdkEvent*)gdk_event, &keyval); + + if(is_editing()) { + if(keyval == GDK_KEY_Escape) { + return _gl_widget.event((GdkEvent *)gdk_event); + } else { + _entry.event((GdkEvent *)gdk_event); + } + return true; + } + + return Gtk::Fixed::on_key_press_event(gdk_event); + } + + bool on_key_release_event(GdkEventKey *gdk_event) override { + if(is_editing()) { + _entry.event((GdkEvent *)gdk_event); + return true; + } + + return Gtk::Fixed::on_key_release_event(gdk_event); + } + + void on_size_allocate(Gtk::Allocation& allocation) override { + Gtk::Fixed::on_size_allocate(allocation); + + _gl_widget.size_allocate(allocation); + + int width, height, min_height, natural_height; + _entry.get_size_request(width, height); + _entry.get_preferred_height(min_height, natural_height); + + Gtk::Allocation entry_rect = _entry.get_allocation(); + entry_rect.set_width(width); + entry_rect.set_height(natural_height); + _entry.size_allocate(entry_rect); + } + + void on_activate() { + if(_receiver->onEditingDone) { + _receiver->onEditingDone(_entry.get_text()); + } + } +}; + +class GtkWindow : public Gtk::Window { + Platform::Window *_receiver; + Gtk::VBox _vbox; + Gtk::MenuBar *_menu_bar = NULL; + Gtk::HBox _hbox; + GtkEditorOverlay _editor_overlay; + Gtk::VScrollbar _scrollbar; + bool _is_under_cursor = false; + bool _is_fullscreen = false; + std::string _tooltip_text; + Gdk::Rectangle _tooltip_area; + +public: + GtkWindow(Platform::Window *receiver) : _receiver(receiver), _editor_overlay(receiver) { + _hbox.pack_start(_editor_overlay, /*expand=*/true, /*fill=*/true); + _hbox.pack_end(_scrollbar, /*expand=*/false, /*fill=*/false); + _vbox.pack_end(_hbox, /*expand=*/true, /*fill=*/true); + add(_vbox); + + _vbox.show(); + _hbox.show(); + _editor_overlay.show(); + get_gl_widget().show(); + + _scrollbar.get_adjustment()->signal_value_changed(). + connect(sigc::mem_fun(this, &GtkWindow::on_scrollbar_value_changed)); + + get_gl_widget().set_has_tooltip(true); + get_gl_widget().signal_query_tooltip(). + connect(sigc::mem_fun(this, &GtkWindow::on_query_tooltip)); + } + + bool is_full_screen() const { + return _is_fullscreen; + } + + Gtk::MenuBar *get_menu_bar() const { + return _menu_bar; + } + + void set_menu_bar(Gtk::MenuBar *menu_bar) { + if(_menu_bar) { + _vbox.remove(*_menu_bar); + } + _menu_bar = menu_bar; + if(_menu_bar) { + _menu_bar->show_all(); + _vbox.pack_start(*_menu_bar, /*expand=*/false, /*fill=*/false); + } + } + + GtkEditorOverlay &get_editor_overlay() { + return _editor_overlay; + } + + GtkGLWidget &get_gl_widget() { + return _editor_overlay.get_gl_widget(); + } + + Gtk::VScrollbar &get_scrollbar() { + return _scrollbar; + } + + void set_tooltip(const std::string &text, const Gdk::Rectangle &rect) { + if(_tooltip_text != text) { + _tooltip_text = text; + _tooltip_area = rect; + get_gl_widget().trigger_tooltip_query(); + } + } + +protected: + bool on_query_tooltip(int x, int y, bool keyboard_tooltip, + const Glib::RefPtr &tooltip) { + tooltip->set_text(_tooltip_text); + tooltip->set_tip_area(_tooltip_area); + return !_tooltip_text.empty() && (keyboard_tooltip || _is_under_cursor); + } + + bool on_enter_notify_event(GdkEventCrossing* gdk_event) override { + _is_under_cursor = true; + + return true; + } + + bool on_leave_notify_event(GdkEventCrossing* gdk_event) override { + _is_under_cursor = false; + + return true; + } + + bool on_delete_event(GdkEventAny* gdk_event) override { + if(_receiver->onClose) { + _receiver->onClose(); + return true; + } + + return false; + } + + bool on_window_state_event(GdkEventWindowState *gdk_event) override { + // window state event is superseded by GdkWindow::state on GTK4 + GdkWindowState new_window_state; + new_window_state = gdk_event->new_window_state; + + _is_fullscreen = new_window_state & GDK_WINDOW_STATE_FULLSCREEN; + if(_receiver->onFullScreen) { + _receiver->onFullScreen(_is_fullscreen); + } + + return true; + } + + void on_scrollbar_value_changed() { + if(_receiver->onScrollbarAdjusted) { + _receiver->onScrollbarAdjusted(_scrollbar.get_adjustment()->get_value()); + } + } +}; + +//----------------------------------------------------------------------------- +// Windows +//----------------------------------------------------------------------------- + +class WindowImplGtk final : public Window { +public: + GtkWindow gtkWindow; + MenuBarRef menuBar; + + WindowImplGtk(Window::Kind kind) : gtkWindow(this) { + switch(kind) { + case Kind::TOPLEVEL: + break; + + case Kind::TOOL: + gtkWindow.set_type_hint(Gdk::WINDOW_TYPE_HINT_UTILITY); + gtkWindow.set_skip_taskbar_hint(true); + gtkWindow.set_skip_pager_hint(true); + break; + } + + auto icon = LoadPng("freedesktop/solvespace-48x48.png"); + auto gdkIcon = + Gdk::Pixbuf::create_from_data(&icon->data[0], Gdk::COLORSPACE_RGB, + icon->format == Pixmap::Format::RGBA, 8, + icon->width, icon->height, icon->stride); + gtkWindow.set_icon(gdkIcon->copy()); + } + + double GetPixelDensity() override { + return gtkWindow.get_screen()->get_resolution(); + } + + double GetDevicePixelRatio() override { + return gtkWindow.get_scale_factor(); + } + + bool IsVisible() override { + return gtkWindow.is_visible(); + } + + void SetVisible(bool visible) override { + if(visible) { + gtkWindow.show(); + } else { + gtkWindow.hide(); + } + } + + void Focus() override { + gtkWindow.present(); + } + + bool IsFullScreen() override { + return gtkWindow.is_full_screen(); + } + + void SetFullScreen(bool fullScreen) override { + if(fullScreen) { + gtkWindow.fullscreen(); + } else { + gtkWindow.unfullscreen(); + } + } + + void SetTitle(const std::string &title) override { + gtkWindow.set_title(PrepareTitle(title)); + } + + void SetMenuBar(MenuBarRef newMenuBar) override { + if(newMenuBar) { + Gtk::MenuBar *gtkMenuBar = &((MenuBarImplGtk*)&*newMenuBar)->gtkMenuBar; + gtkWindow.set_menu_bar(gtkMenuBar); + } else { + gtkWindow.set_menu_bar(NULL); + } + menuBar = newMenuBar; + } + + void GetContentSize(double *width, double *height) override { + *width = gtkWindow.get_gl_widget().get_allocated_width(); + *height = gtkWindow.get_gl_widget().get_allocated_height(); + } + + void SetMinContentSize(double width, double height) override { + gtkWindow.get_gl_widget().set_size_request((int)width, (int)height); + } + + void FreezePosition(SettingsRef settings, const std::string &key) override { + if(!gtkWindow.is_visible()) return; + + int left, top, width, height; + gtkWindow.get_position(left, top); + gtkWindow.get_size(width, height); + bool isMaximized = gtkWindow.is_maximized(); + + settings->FreezeInt(key + "_Left", left); + settings->FreezeInt(key + "_Top", top); + settings->FreezeInt(key + "_Width", width); + settings->FreezeInt(key + "_Height", height); + settings->FreezeBool(key + "_Maximized", isMaximized); + } + + void ThawPosition(SettingsRef settings, const std::string &key) override { + int left, top, width, height; + gtkWindow.get_position(left, top); + gtkWindow.get_size(width, height); + + left = settings->ThawInt(key + "_Left", left); + top = settings->ThawInt(key + "_Top", top); + width = settings->ThawInt(key + "_Width", width); + height = settings->ThawInt(key + "_Height", height); + + gtkWindow.move(left, top); + gtkWindow.resize(width, height); + + if(settings->ThawBool(key + "_Maximized", false)) { + gtkWindow.maximize(); + } + } + + void SetCursor(Cursor cursor) override { + std::string cursor_name; + switch(cursor) { + case Cursor::POINTER: cursor_name = "default"; break; + case Cursor::HAND: cursor_name = "pointer"; break; + default: ssassert(false, "Unexpected cursor"); + } + + auto gdkWindow = gtkWindow.get_gl_widget().get_window(); + if(gdkWindow) { + gdkWindow->set_cursor(Gdk::Cursor::create(gdkWindow->get_display(), cursor_name.c_str())); +// gdkWindow->get_display() + } + } + + void SetTooltip(const std::string &text, double x, double y, + double width, double height) override { + gtkWindow.set_tooltip(text, { (int)x, (int)y, (int)width, (int)height }); + } + + bool IsEditorVisible() override { + return gtkWindow.get_editor_overlay().is_editing(); + } + + void ShowEditor(double x, double y, double fontHeight, double minWidth, + bool isMonospace, const std::string &text) override { + gtkWindow.get_editor_overlay().start_editing( + (int)x, (int)y, (int)fontHeight, (int)minWidth, isMonospace, text); + } + + void HideEditor() override { + gtkWindow.get_editor_overlay().stop_editing(); + } + + void SetScrollbarVisible(bool visible) override { + if(visible) { + gtkWindow.get_scrollbar().show(); + } else { + gtkWindow.get_scrollbar().hide(); + } + } + + void ConfigureScrollbar(double min, double max, double pageSize) override { + auto adjustment = gtkWindow.get_scrollbar().get_adjustment(); + adjustment->configure(adjustment->get_value(), min, max, 1, 4, pageSize); + } + + double GetScrollbarPosition() override { + return gtkWindow.get_scrollbar().get_adjustment()->get_value(); + } + + void SetScrollbarPosition(double pos) override { + return gtkWindow.get_scrollbar().get_adjustment()->set_value(pos); + } + + void Invalidate() override { + gtkWindow.get_gl_widget().queue_render(); + } +}; + +WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { + auto window = std::make_shared(kind); + if(parentWindow) { + window->gtkWindow.set_transient_for( + std::static_pointer_cast(parentWindow)->gtkWindow); + } + return window; +} + +//----------------------------------------------------------------------------- +// 3DConnexion support +//----------------------------------------------------------------------------- + +void Open3DConnexion() {} +void Close3DConnexion() {} + +#if defined(HAVE_SPACEWARE) && (defined(GDK_WINDOWING_X11) || defined(GDK_WINDOWING_WAYLAND)) +static void ProcessSpnavEvent(WindowImplGtk *window, const spnav_event &spnavEvent, bool shiftDown, bool controlDown) { + switch(spnavEvent.type) { + case SPNAV_EVENT_MOTION: { + SixDofEvent event = {}; + event.type = SixDofEvent::Type::MOTION; + event.translationX = (double)spnavEvent.motion.x; + event.translationY = (double)spnavEvent.motion.y; + event.translationZ = (double)spnavEvent.motion.z * -1.0; + event.rotationX = (double)spnavEvent.motion.rx * 0.001; + event.rotationY = (double)spnavEvent.motion.ry * 0.001; + event.rotationZ = (double)spnavEvent.motion.rz * -0.001; + event.shiftDown = shiftDown; + event.controlDown = controlDown; + if(window->onSixDofEvent) { + window->onSixDofEvent(event); + } + break; + } + + case SPNAV_EVENT_BUTTON: + SixDofEvent event = {}; + if(spnavEvent.button.press) { + event.type = SixDofEvent::Type::PRESS; + } else { + event.type = SixDofEvent::Type::RELEASE; + } + switch(spnavEvent.button.bnum) { + case 0: event.button = SixDofEvent::Button::FIT; break; + default: return; + } + event.shiftDown = shiftDown; + event.controlDown = controlDown; + if(window->onSixDofEvent) { + window->onSixDofEvent(event); + } + break; + } +} + +static GdkFilterReturn GdkSpnavFilter(GdkXEvent *gdkXEvent, GdkEvent *gdkEvent, gpointer data) { + XEvent *xEvent = (XEvent *)gdkXEvent; + WindowImplGtk *window = (WindowImplGtk *)data; + bool shiftDown = (xEvent->xmotion.state & ShiftMask) != 0; + bool controlDown = (xEvent->xmotion.state & ControlMask) != 0; + + spnav_event spnavEvent; + if(spnav_x11_event(xEvent, &spnavEvent)) { + ProcessSpnavEvent(window, spnavEvent, shiftDown, controlDown); + return GDK_FILTER_REMOVE; + } + return GDK_FILTER_CONTINUE; +} + +static gboolean ConsumeSpnavQueue(GIOChannel *, GIOCondition, gpointer data) { + WindowImplGtk *window = (WindowImplGtk *)data; + Glib::RefPtr gdkWindow = window->gtkWindow.get_window(); + + // We don't get modifier state through the socket. + int x, y; + Gdk::ModifierType mask{}; +#if GTK_CHECK_VERSION(3, 20, 0) + Glib::RefPtr device = gdkWindow->get_display()->get_default_seat()->get_pointer(); +#else + Glib::RefPtr device = gdkWindow->get_display()->get_device_manager()->get_client_pointer(); +#endif + gdkWindow->get_device_position(device, x, y, mask); + bool shiftDown = (mask & Gdk::SHIFT_MASK) != 0; + bool controlDown = (mask & Gdk::CONTROL_MASK) != 0; + + spnav_event spnavEvent; + while(spnav_poll_event(&spnavEvent)) { + ProcessSpnavEvent(window, spnavEvent, shiftDown, controlDown); + } + return TRUE; +} + +void Request3DConnexionEventsForWindow(WindowRef window) { + std::shared_ptr windowImpl = + std::static_pointer_cast(window); + + Glib::RefPtr gdkWindow = windowImpl->gtkWindow.get_window(); +#if defined(GDK_WINDOWING_X11) + if(GDK_IS_X11_DISPLAY(gdkWindow->get_display()->gobj())) { + if(spnav_x11_open(gdk_x11_get_default_xdisplay(), + gdk_x11_window_get_xid(gdkWindow->gobj())) != -1) { + gdkWindow->add_filter(GdkSpnavFilter, windowImpl.get()); + } else if(spnav_open() != -1) { + g_io_add_watch(g_io_channel_unix_new(spnav_fd()), G_IO_IN, + ConsumeSpnavQueue, windowImpl.get()); + } + } +#endif +#if defined(GDK_WINDOWING_WAYLAND) + if(GDK_IS_WAYLAND_DISPLAY(gdkWindow->get_display()->gobj())) { + if(spnav_open() != -1) { + g_io_add_watch(g_io_channel_unix_new(spnav_fd()), G_IO_IN, + ConsumeSpnavQueue, windowImpl.get()); + } + } +#endif + +} +#else +void Request3DConnexionEventsForWindow(WindowRef window) {} +#endif + +//----------------------------------------------------------------------------- +// Message dialogs +//----------------------------------------------------------------------------- + +class MessageDialogImplGtk; + +static std::vector> shownMessageDialogs; + +class MessageDialogImplGtk final : public MessageDialog, + public std::enable_shared_from_this { +public: + Gtk::Image gtkImage; + Gtk::MessageDialog gtkDialog; + + MessageDialogImplGtk(Gtk::Window &parent) + : gtkDialog(parent, "", /*use_markup=*/false, Gtk::MESSAGE_INFO, + Gtk::BUTTONS_NONE, /*modal=*/true) + { + SetTitle("Message"); + } + + void SetType(Type type) override { + switch(type) { + case Type::INFORMATION: + gtkImage.set_from_icon_name("dialog-information", Gtk::ICON_SIZE_DIALOG); + break; + + case Type::QUESTION: + gtkImage.set_from_icon_name("dialog-question", Gtk::ICON_SIZE_DIALOG); + break; + + case Type::WARNING: + gtkImage.set_from_icon_name("dialog-warning", Gtk::ICON_SIZE_DIALOG); + break; + + case Type::ERROR: + gtkImage.set_from_icon_name("dialog-error", Gtk::ICON_SIZE_DIALOG); + break; + } + gtkDialog.set_image(gtkImage); + } + + void SetTitle(std::string title) override { + gtkDialog.set_title(PrepareTitle(title)); + } + + void SetMessage(std::string message) override { + gtkDialog.set_message(message); + } + + void SetDescription(std::string description) override { + gtkDialog.set_secondary_text(description); + } + + void AddButton(std::string label, Response response, bool isDefault) override { + int responseId = 0; + switch(response) { + case Response::NONE: ssassert(false, "Unexpected response"); + case Response::OK: responseId = Gtk::RESPONSE_OK; break; + case Response::YES: responseId = Gtk::RESPONSE_YES; break; + case Response::NO: responseId = Gtk::RESPONSE_NO; break; + case Response::CANCEL: responseId = Gtk::RESPONSE_CANCEL; break; + } + gtkDialog.add_button(PrepareMnemonics(label), responseId); + if(isDefault) { + gtkDialog.set_default_response(responseId); + } + } + + Response ProcessResponse(int gtkResponse) { + Response response; + switch(gtkResponse) { + case Gtk::RESPONSE_OK: response = Response::OK; break; + case Gtk::RESPONSE_YES: response = Response::YES; break; + case Gtk::RESPONSE_NO: response = Response::NO; break; + case Gtk::RESPONSE_CANCEL: response = Response::CANCEL; break; + + case Gtk::RESPONSE_NONE: + case Gtk::RESPONSE_CLOSE: + case Gtk::RESPONSE_DELETE_EVENT: + response = Response::NONE; + break; + + default: ssassert(false, "Unexpected response"); + } + + if(onResponse) { + onResponse(response); + } + return response; + } + + void ShowModal() override { + gtkDialog.signal_hide().connect([this] { + auto it = std::remove(shownMessageDialogs.begin(), shownMessageDialogs.end(), + shared_from_this()); + shownMessageDialogs.erase(it); + }); + shownMessageDialogs.push_back(shared_from_this()); + + gtkDialog.signal_response().connect([this](int gtkResponse) { + ProcessResponse(gtkResponse); + gtkDialog.hide(); + }); + gtkDialog.show(); + } + + Response RunModal() override { + return ProcessResponse(gtkDialog.run()); + } +}; + +MessageDialogRef CreateMessageDialog(WindowRef parentWindow) { + return std::make_shared( + std::static_pointer_cast(parentWindow)->gtkWindow); +} + +//----------------------------------------------------------------------------- +// File dialogs +//----------------------------------------------------------------------------- + +class FileDialogImplGtk : public FileDialog { +public: + Gtk::FileChooser *gtkChooser; + std::vector extensions; + + void InitFileChooser(Gtk::FileChooser &chooser) { + gtkChooser = &chooser; + gtkChooser->property_filter().signal_changed(). + connect(sigc::mem_fun(this, &FileDialogImplGtk::FilterChanged)); + } + + void SetCurrentName(std::string name) override { + gtkChooser->set_current_name(name); + } + + Platform::Path GetFilename() override { + return Path::From(gtkChooser->get_filename()); + } + + void SetFilename(Platform::Path path) override { + gtkChooser->set_filename(path.raw); + } + + void SuggestFilename(Platform::Path path) override { + gtkChooser->set_current_name(path.FileStem()+"."+GetExtension()); + } + + void AddFilter(std::string name, std::vector extensions) override { + Glib::RefPtr gtkFilter = Gtk::FileFilter::create(); + Glib::ustring desc; + for(auto extension : extensions) { + Glib::ustring pattern = "*"; + if(!extension.empty()) { + pattern = "*." + extension; + gtkFilter->add_pattern(pattern); + gtkFilter->add_pattern(Glib::ustring(pattern).uppercase()); + } + if(!desc.empty()) { + desc += ", "; + } + desc += pattern; + } + gtkFilter->set_name(name + " (" + desc + ")"); + + this->extensions.push_back(extensions.front()); + gtkChooser->add_filter(gtkFilter); + } + + std::string GetExtension() { + auto filters = gtkChooser->list_filters(); + size_t filterIndex = + std::find(filters.begin(), filters.end(), gtkChooser->get_filter()) - + filters.begin(); + if(filterIndex < extensions.size()) { + return extensions[filterIndex]; + } else { + return extensions.front(); + } + } + + void SetExtension(std::string extension) { + auto filters = gtkChooser->list_filters(); + size_t extensionIndex = + std::find(extensions.begin(), extensions.end(), extension) - + extensions.begin(); + if(extensionIndex < filters.size()) { + gtkChooser->set_filter(filters[extensionIndex]); + } else { + gtkChooser->set_filter(filters.front()); + } + } + +//TODO: This is not getting called when the extension selection is changed. + void FilterChanged() { + std::string extension = GetExtension(); + if(extension.empty()) + return; + + Platform::Path path = GetFilename(); + if(gtkChooser->get_action() != Gtk::FILE_CHOOSER_ACTION_OPEN) { + SetCurrentName(path.WithExtension(extension).FileName()); + } + } + + void FreezeChoices(SettingsRef settings, const std::string &key) override { + settings->FreezeString("Dialog_" + key + "_Folder", + gtkChooser->get_current_folder()); + settings->FreezeString("Dialog_" + key + "_Filter", GetExtension()); + } + + void ThawChoices(SettingsRef settings, const std::string &key) override { + gtkChooser->set_current_folder(settings->ThawString("Dialog_" + key + "_Folder")); + SetExtension(settings->ThawString("Dialog_" + key + "_Filter")); + } + + void CheckForUntitledFile() { + if(gtkChooser->get_action() == Gtk::FILE_CHOOSER_ACTION_SAVE && + Path::From(gtkChooser->get_current_name()).FileStem().empty()) { + gtkChooser->set_current_name(std::string(_("untitled")) + "." + GetExtension()); + } + } +}; + +class FileDialogGtkImplGtk final : public FileDialogImplGtk { +public: + Gtk::FileChooserDialog gtkDialog; + + FileDialogGtkImplGtk(Gtk::Window >kParent, bool isSave) + : gtkDialog(gtkParent, + isSave ? C_("title", "Save File") + : C_("title", "Open File"), + isSave ? Gtk::FILE_CHOOSER_ACTION_SAVE + : Gtk::FILE_CHOOSER_ACTION_OPEN) { + gtkDialog.add_button(C_("button", "_Cancel"), Gtk::RESPONSE_CANCEL); + gtkDialog.add_button(isSave ? C_("button", "_Save") + : C_("button", "_Open"), Gtk::RESPONSE_OK); + gtkDialog.set_default_response(Gtk::RESPONSE_OK); + if(isSave) { + gtkDialog.set_do_overwrite_confirmation(true); + } + InitFileChooser(gtkDialog); + } + + void SetTitle(std::string title) override { + gtkDialog.set_title(PrepareTitle(title)); + } + + bool RunModal() override { + CheckForUntitledFile(); + if(gtkDialog.run() == Gtk::RESPONSE_OK) { + return true; + } else { + return false; + } + } +}; + +#if defined(HAVE_GTK_FILECHOOSERNATIVE) + +class FileDialogNativeImplGtk final : public FileDialogImplGtk { +public: + Glib::RefPtr gtkNative; + + FileDialogNativeImplGtk(Gtk::Window >kParent, bool isSave) { + gtkNative = Gtk::FileChooserNative::create( + isSave ? C_("title", "Save File") + : C_("title", "Open File"), + gtkParent, + isSave ? Gtk::FILE_CHOOSER_ACTION_SAVE + : Gtk::FILE_CHOOSER_ACTION_OPEN, + isSave ? C_("button", "_Save") + : C_("button", "_Open"), + C_("button", "_Cancel")); + if(isSave) { + gtkNative->set_do_overwrite_confirmation(true); + } + // Seriously, GTK?! + InitFileChooser(*gtkNative.operator->()); + } + + void SetTitle(std::string title) override { + gtkNative->set_title(PrepareTitle(title)); + } + + bool RunModal() override { + CheckForUntitledFile(); + if(gtkNative->run() == Gtk::RESPONSE_ACCEPT) { + return true; + } else { + return false; + } + } +}; + +#endif + +#if defined(HAVE_GTK_FILECHOOSERNATIVE) +# define FILE_DIALOG_IMPL FileDialogNativeImplGtk +#else +# define FILE_DIALOG_IMPL FileDialogGtkImplGtk +#endif + +FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) { + Gtk::Window >kParent = std::static_pointer_cast(parentWindow)->gtkWindow; + return std::make_shared(gtkParent, /*isSave=*/false); +} + +FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) { + Gtk::Window >kParent = std::static_pointer_cast(parentWindow)->gtkWindow; + return std::make_shared(gtkParent, /*isSave=*/true); +} + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +std::vector GetFontFiles() { + std::vector fonts; + + // fontconfig is already initialized by GTK + FcPattern *pat = FcPatternCreate(); + FcObjectSet *os = FcObjectSetBuild(FC_FILE, (char *)0); + FcFontSet *fs = FcFontList(0, pat, os); + + for(int i = 0; i < fs->nfont; i++) { + FcChar8 *filenameFC = FcPatternFormat(fs->fonts[i], (const FcChar8*) "%{file}"); + fonts.push_back(Platform::Path::From((const char *)filenameFC)); + FcStrFree(filenameFC); + } + + FcFontSetDestroy(fs); + FcObjectSetDestroy(os); + FcPatternDestroy(pat); + + return fonts; +} + +void OpenInBrowser(const std::string &url) { + // first param should be our window? + gtk_show_uri_on_window(NULL, url.c_str(), GDK_CURRENT_TIME, NULL); +} + +Gtk::Main *gtkMain; + +std::vector InitGui(int argc, char **argv) { + // It would in principle be possible to judiciously use Glib::filename_{from,to}_utf8, + // but it's not really worth the effort. + // The setlocale() call is necessary for Glib::get_charset() to detect the system + // character set; otherwise it thinks it is always ANSI_X3.4-1968. + // We set it back to C after all so that printf() and friends behave in a consistent way. + setlocale(LC_ALL, ""); + if(!Glib::get_charset()) { + dbp("Sorry, only UTF-8 locales are supported."); + exit(1); + } + setlocale(LC_ALL, "C"); + + // Let GTK parse arguments and update argc/argv. (They're passed by reference.) + gtkMain = new Gtk::Main(argc, argv, /*set_locale=*/false); + + // Now that GTK arguments are removed, grab arguments for ourselves. + std::vector args = InitCli(argc, argv); + + // Add our application-specific styles, to override GTK defaults. + Glib::RefPtr style_provider = Gtk::CssProvider::create(); + style_provider->load_from_data(R"( + entry { + background: white; + color: black; + } + )"); + Gtk::StyleContext::add_provider_for_screen( + Gdk::Screen::get_default(), style_provider, + 600 /*Gtk::STYLE_PROVIDER_PRIORITY_APPLICATION*/); + + // Set locale from user preferences. + // This apparently only consults the LANGUAGE environment variable. + const char* const* langNames = g_get_language_names(); + while(*langNames) { + if(SetLocale(*langNames++)) break; + } + if(!*langNames) { + SetLocale("en_US"); + } + + return args; +} + +void RunGui() { + Gtk::Main::run(); +} + +void ExitGui() { + Gtk::Main::quit(); +} + +void ClearGui() { + delete gtkMain; +} + +} +} diff --git a/src/platform/guihtml.cpp b/src/platform/guihtml.cpp new file mode 100644 index 0000000..2625f8e --- /dev/null +++ b/src/platform/guihtml.cpp @@ -0,0 +1,1449 @@ +//----------------------------------------------------------------------------- +// The Emscripten-based implementation of platform-dependent GUI functionality. +// +// Copyright 2018 whitequark +//----------------------------------------------------------------------------- +#include +#include +#include +#include +#include "config.h" +#include "solvespace.h" + +using namespace emscripten; + +EMSCRIPTEN_BINDINGS(solvespace) { + emscripten::class_>("VoidFunctor") + .constructor<>() + .function("opcall", &std::function::operator()); + + emscripten::class_>("Void1Functor") + .constructor<>() + .function("opcall", &std::function::operator()); +} + +namespace SolveSpace { +namespace Platform { + +//----------------------------------------------------------------------------- +// Emscripten API bridging +//----------------------------------------------------------------------------- + +#define sscheck(expr) do { \ + EMSCRIPTEN_RESULT emResult = (EMSCRIPTEN_RESULT)(expr); \ + if(emResult < 0) \ + HandleError(__FILE__, __LINE__, __func__, #expr, emResult); \ + } while(0) + +static void HandleError(const char *file, int line, const char *function, const char *expr, + EMSCRIPTEN_RESULT emResult) { + const char *error = "Unknown error"; + switch(emResult) { + case EMSCRIPTEN_RESULT_DEFERRED: error = "Deferred"; break; + case EMSCRIPTEN_RESULT_NOT_SUPPORTED: error = "Not supported"; break; + case EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED: error = "Failed (not deferred)"; break; + case EMSCRIPTEN_RESULT_INVALID_TARGET: error = "Invalid target"; break; + case EMSCRIPTEN_RESULT_UNKNOWN_TARGET: error = "Unknown target"; break; + case EMSCRIPTEN_RESULT_INVALID_PARAM: error = "Invalid parameter"; break; + case EMSCRIPTEN_RESULT_FAILED: error = "Failed"; break; + case EMSCRIPTEN_RESULT_NO_DATA: error = "No data"; break; + } + + std::string message; + message += ssprintf("File %s, line %u, function %s:\n", file, line, function); + message += ssprintf("Emscripten API call failed: %s.\n", expr); + message += ssprintf("Error: %s\n", error); + FatalError(message); +} + +static val Wrap(const std::function &functor) { + return val(functor)["opcall"].call("bind", val(functor)); +} + +static void RegisterEventListener(const val &target, std::string event, std::function functor) { + std::function wrapper = [functor](val) { if(functor) functor(); }; + val wrapped = val(wrapper)["opcall"].call("bind", val(wrapper)); + target.call("addEventListener", event, wrapped); +} + +//----------------------------------------------------------------------------- +// Fatal errors +//----------------------------------------------------------------------------- + +void FatalError(const std::string &message) { + dbp("%s", message.c_str()); +#ifndef NDEBUG + emscripten_debugger(); +#endif + abort(); +} + +//----------------------------------------------------------------------------- +// Settings +//----------------------------------------------------------------------------- + +class SettingsImplHtml : public Settings { +public: + void FreezeInt(const std::string &key, uint32_t value) { + val::global("localStorage").call("setItem", key, value); + } + + uint32_t ThawInt(const std::string &key, uint32_t defaultValue = 0) { + val value = val::global("localStorage").call("getItem", key); + if(value == val::null()) + return defaultValue; + return val::global("parseInt")(value, 0).as(); + } + + void FreezeFloat(const std::string &key, double value) { + val::global("localStorage").call("setItem", key, value); + } + + double ThawFloat(const std::string &key, double defaultValue = 0.0) { + val value = val::global("localStorage").call("getItem", key); + if(value == val::null()) + return defaultValue; + return val::global("parseFloat")(value).as(); + } + + void FreezeString(const std::string &key, const std::string &value) { + val::global("localStorage").call("setItem", key, value); + } + + std::string ThawString(const std::string &key, + const std::string &defaultValue = "") { + val value = val::global("localStorage").call("getItem", key); + if(value == val::null()) { + return defaultValue; + } + return value.as(); + } +}; + +SettingsRef GetSettings() { + return std::make_shared(); +} + +//----------------------------------------------------------------------------- +// Timers +//----------------------------------------------------------------------------- + +class TimerImplHtml : public Timer { +public: + static void Callback(void *arg) { + TimerImplHtml *timer = (TimerImplHtml *)arg; + if(timer->onTimeout) { + timer->onTimeout(); + } + } + + void RunAfter(unsigned milliseconds) override { + emscripten_async_call(TimerImplHtml::Callback, this, milliseconds + 1); + } + + void RunAfterNextFrame() override { + emscripten_async_call(TimerImplHtml::Callback, this, 0); + } + + void RunAfterProcessingEvents() override { + emscripten_push_uncounted_main_loop_blocker(TimerImplHtml::Callback, this); + } +}; + +TimerRef CreateTimer() { + return std::unique_ptr(new TimerImplHtml); +} + +//----------------------------------------------------------------------------- +// Menus +//----------------------------------------------------------------------------- + +class MenuItemImplHtml : public MenuItem { +public: + val htmlMenuItem; + + MenuItemImplHtml() : + htmlMenuItem(val::global("document").call("createElement", val("li"))) + {} + + void SetAccelerator(KeyboardEvent accel) override { + val htmlAccel = htmlMenuItem.call("querySelector", val(".accel")); + if(htmlAccel.as()) { + htmlAccel.call("remove"); + } + htmlAccel = val::global("document").call("createElement", val("span")); + htmlAccel.call("setAttribute", val("class"), val("accel")); + htmlAccel.set("innerText", AcceleratorDescription(accel)); + htmlMenuItem.call("appendChild", htmlAccel); + } + + void SetIndicator(Indicator type) override { + val htmlClasses = htmlMenuItem["classList"]; + htmlClasses.call("remove", val("check")); + htmlClasses.call("remove", val("radio")); + switch(type) { + case Indicator::NONE: + break; + + case Indicator::CHECK_MARK: + htmlClasses.call("add", val("check")); + break; + + case Indicator::RADIO_MARK: + htmlClasses.call("add", val("radio")); + break; + } + } + + void SetEnabled(bool enabled) override { + if(enabled) { + htmlMenuItem["classList"].call("remove", val("disabled")); + } else { + htmlMenuItem["classList"].call("add", val("disabled")); + } + } + + void SetActive(bool active) override { + if(active) { + htmlMenuItem["classList"].call("add", val("active")); + } else { + htmlMenuItem["classList"].call("remove", val("active")); + } + } +}; + +class MenuImplHtml; + +static std::shared_ptr popupMenuOnScreen; + +class MenuImplHtml : public Menu, + public std::enable_shared_from_this { +public: + val htmlMenu; + + std::vector> menuItems; + std::vector> subMenus; + + std::function popupDismissFunc; + + MenuImplHtml() : + htmlMenu(val::global("document").call("createElement", val("ul"))) + { + htmlMenu["classList"].call("add", val("menu")); + } + + MenuItemRef AddItem(const std::string &label, std::function onTrigger, + bool mnemonics = true) override { + std::shared_ptr menuItem = std::make_shared(); + menuItems.push_back(menuItem); + menuItem->onTrigger = onTrigger; + + if(mnemonics) { + val::global("window").call("setLabelWithMnemonic", menuItem->htmlMenuItem, + label); + } else { + val htmlLabel = val::global("document").call("createElement", val("span")); + htmlLabel["classList"].call("add", val("label")); + htmlLabel.set("innerText", label); + menuItem->htmlMenuItem.call("appendChild", htmlLabel); + } + RegisterEventListener(menuItem->htmlMenuItem, "trigger", [menuItem]() { + if(menuItem->onTrigger) { + menuItem->onTrigger(); + } + }); + htmlMenu.call("appendChild", menuItem->htmlMenuItem); + return menuItem; + } + + std::shared_ptr AddSubMenu(const std::string &label) override { + val htmlMenuItem = val::global("document").call("createElement", val("li")); + val::global("window").call("setLabelWithMnemonic", htmlMenuItem, label); + htmlMenuItem["classList"].call("add", val("has-submenu")); + htmlMenu.call("appendChild", htmlMenuItem); + + std::shared_ptr subMenu = std::make_shared(); + subMenus.push_back(subMenu); + htmlMenuItem.call("appendChild", subMenu->htmlMenu); + return subMenu; + } + + void AddSeparator() override { + val htmlSeparator = val::global("document").call("createElement", val("li")); + htmlSeparator["classList"].call("add", val("separator")); + htmlMenu.call("appendChild", htmlSeparator); + } + + void PopUp() override { + if(popupMenuOnScreen) { + popupMenuOnScreen->htmlMenu.call("remove"); + popupMenuOnScreen = NULL; + } + + EmscriptenMouseEvent emStatus = {}; + sscheck(emscripten_get_mouse_status(&emStatus)); + htmlMenu["classList"].call("add", val("popup")); + htmlMenu["style"].set("left", std::to_string(emStatus.clientX) + "px"); + htmlMenu["style"].set("top", std::to_string(emStatus.clientY) + "px"); + + val::global("document")["body"].call("appendChild", htmlMenu); + popupMenuOnScreen = shared_from_this(); + } + + void Clear() override { + while(htmlMenu["childElementCount"].as() > 0) { + htmlMenu["firstChild"].call("remove"); + } + } +}; + +MenuRef CreateMenu() { + return std::make_shared(); +} + +class MenuBarImplHtml final : public MenuBar { +public: + val htmlMenuBar; + + std::vector> subMenus; + + MenuBarImplHtml() : + htmlMenuBar(val::global("document").call("createElement", val("ul"))) + { + htmlMenuBar["classList"].call("add", val("menu")); + htmlMenuBar["classList"].call("add", val("menubar")); + } + + std::shared_ptr AddSubMenu(const std::string &label) override { + val htmlMenuItem = val::global("document").call("createElement", val("li")); + val::global("window").call("setLabelWithMnemonic", htmlMenuItem, label); + htmlMenuBar.call("appendChild", htmlMenuItem); + + std::shared_ptr subMenu = std::make_shared(); + subMenus.push_back(subMenu); + htmlMenuItem.call("appendChild", subMenu->htmlMenu); + return subMenu; + } + + void Clear() override { + while(htmlMenuBar["childElementCount"].as() > 0) { + htmlMenuBar["firstChild"].call("remove"); + } + } +}; + +MenuBarRef GetOrCreateMainMenu(bool *unique) { + *unique = false; + return std::make_shared(); +} + +//----------------------------------------------------------------------------- +// Windows +//----------------------------------------------------------------------------- + +class TouchEventHelper { +public: + // FIXME(emscripten): Workaround. touchstart and touchend repeats two times. + bool touchActionStarted = false; + + int previousNumTouches = 0; + + double centerX = 0; + double centerY = 0; + + // double startPinchDistance = 0; + double previousPinchDistance = 0; + + std::function onPointerDown; + std::function onPointerMove; + std::function onPointerUp; + std::function onScroll; + + void clear(void) { + touchActionStarted = false; + previousNumTouches = 0; + centerX = 0; + centerY = 0; + // startPinchDistance = 0; + previousPinchDistance = 0; + } + + void calculateCenterPosition(const EmscriptenTouchEvent& emEvent, double& dst_x, double& dst_y) { + double x = 0; + double y = 0; + for (int i = 0; i < emEvent.numTouches; i++) { + x += emEvent.touches[i].targetX; + y += emEvent.touches[i].targetY; + } + dst_x = x / emEvent.numTouches; + dst_y = y / emEvent.numTouches; + } + + void calculatePinchDistance(const EmscriptenTouchEvent& emEvent, double& dst_distance) { + if (emEvent.numTouches < 2) { + return; + } + double x1 = emEvent.touches[0].targetX; + double y1 = emEvent.touches[0].targetY; + double x2 = emEvent.touches[1].targetX; + double y2 = emEvent.touches[1].targetY; + dst_distance = std::sqrt(std::pow(x1 - x2, 2) + std::pow(y1 - y2, 2)); + } + + void createMouseEventPRESS(const EmscriptenTouchEvent& emEvent, MouseEvent& dst_mouseevent) { + double x = 0, y = 0; + this->calculateCenterPosition(emEvent, x, y); + this->centerX = x; + this->centerY = y; + this->touchActionStarted = true; + this->previousNumTouches = emEvent.numTouches; + dst_mouseevent.type = MouseEvent::Type::PRESS; + dst_mouseevent.x = x; + dst_mouseevent.y = y; + dst_mouseevent.shiftDown = emEvent.shiftKey; + dst_mouseevent.controlDown = emEvent.ctrlKey; + switch(emEvent.numTouches) { + case 1: + dst_mouseevent.button = MouseEvent::Button::LEFT; + break; + case 2: { + dst_mouseevent.button = MouseEvent::Button::RIGHT; + // double distance = 0; + this->calculatePinchDistance(emEvent, this->previousPinchDistance); + // this->startPinchDistance = distance; + // this->previousPinchDistance = distance; + break; + } + default: + dst_mouseevent.button = MouseEvent::Button::MIDDLE; + break; + } + } + + void createMouseEventRELEASE(const EmscriptenTouchEvent& emEvent, MouseEvent& dst_mouseevent) { + this->calculateCenterPosition(emEvent, this->centerX, this->centerY); + this->previousNumTouches = 0; + dst_mouseevent.type = MouseEvent::Type::RELEASE; + dst_mouseevent.x = this->centerX; + dst_mouseevent.y = this->centerY; + dst_mouseevent.shiftDown = emEvent.shiftKey; + dst_mouseevent.controlDown = emEvent.ctrlKey; + switch(this->previousNumTouches) { + case 1: + dst_mouseevent.button = MouseEvent::Button::LEFT; + break; + case 2: + dst_mouseevent.button = MouseEvent::Button::RIGHT; + break; + default: + dst_mouseevent.button = MouseEvent::Button::MIDDLE; + break; + } + } + + void createMouseEventMOTION(const EmscriptenTouchEvent& emEvent, MouseEvent& dst_mouseevent) { + dst_mouseevent.type = MouseEvent::Type::MOTION; + this->calculateCenterPosition(emEvent, this->centerX, this->centerY); + dst_mouseevent.x = this->centerX; + dst_mouseevent.y = this->centerY; + dst_mouseevent.shiftDown = emEvent.shiftKey; + dst_mouseevent.controlDown = emEvent.ctrlKey; + switch(emEvent.numTouches) { + case 1: + dst_mouseevent.button = MouseEvent::Button::LEFT; + break; + case 2: + dst_mouseevent.button = MouseEvent::Button::RIGHT; + break; + default: + dst_mouseevent.button = MouseEvent::Button::MIDDLE; + break; + } + } + + void createMouseEventSCROLL(const EmscriptenTouchEvent& emEvent, MouseEvent& event) { + event.type = MouseEvent::Type::SCROLL_VERT; + double newDistance = 0; + this->calculatePinchDistance(emEvent, newDistance); + this->calculateCenterPosition(emEvent, this->centerX, this->centerY); + event.x = this->centerX; + event.y = this->centerY; + event.shiftDown = emEvent.shiftKey; + event.controlDown = emEvent.ctrlKey; + // FIXME(emscripten): best value range for scrollDelta ? + event.scrollDelta = (newDistance - this->previousPinchDistance) / 25.0; + if (std::abs(event.scrollDelta) > 2) { + event.scrollDelta = 2; + if (std::signbit(event.scrollDelta)) { + event.scrollDelta *= -1.0; + } + } + this->previousPinchDistance = newDistance; + } + + void onTouchStart(const EmscriptenTouchEvent& emEvent, void* callbackParameter) { + if (this->touchActionStarted) { + // dbp("onTouchStart(): Break due to already started."); + return; + } + + MouseEvent event; + this->createMouseEventPRESS(emEvent, event); + this->previousNumTouches = emEvent.numTouches; + if (this->onPointerDown) { + // dbp("onPointerDown(): numTouches=%d, timestamp=%f", emEvent.numTouches, emEvent.timestamp); + this->onPointerDown(&event, callbackParameter); + } + } + + void onTouchMove(const EmscriptenTouchEvent& emEvent, void* callbackParameter) { + this->calculateCenterPosition(emEvent, this->centerX, this->centerY); + int newNumTouches = emEvent.numTouches; + if (newNumTouches != this->previousNumTouches) { + MouseEvent releaseEvent; + + this->createMouseEventRELEASE(emEvent, releaseEvent); + if (this->onPointerUp) { + // dbp("onPointerUp(): numTouches=%d, timestamp=%f", emEvent.numTouches, emEvent.timestamp); + this->onPointerUp(&releaseEvent, callbackParameter); + } + + MouseEvent pressEvent; + + this->createMouseEventPRESS(emEvent, pressEvent); + if (this->onPointerDown) { + // dbp("onPointerDown(): numTouches=%d, timestamp=%f", emEvent.numTouches, emEvent.timestamp); + this->onPointerDown(&pressEvent, callbackParameter); + } + } + + MouseEvent motionEvent = { }; + this->createMouseEventMOTION(emEvent, motionEvent); + + if (this->onPointerMove) { + // dbp("onPointerMove(): numTouches=%d, timestamp=%f", emEvent.numTouches, emEvent.timestamp); + this->onPointerMove(&motionEvent, callbackParameter); + } + + if (emEvent.numTouches == 2) { + MouseEvent scrollEvent; + this->createMouseEventSCROLL(emEvent, scrollEvent); + if (scrollEvent.scrollDelta != 0) { + if (this->onScroll) { + // dbp("Scroll %f", scrollEvent.scrollDelta); + this->onScroll(&scrollEvent, callbackParameter); + } + } + } + + this->previousNumTouches = emEvent.numTouches; + } + + void onTouchEnd(const EmscriptenTouchEvent& emEvent, void* callbackParameter) { + if (!this->touchActionStarted) { + return; + } + + MouseEvent releaseEvent = { }; + this->createMouseEventRELEASE(emEvent, releaseEvent); + + if (this->onPointerUp) { + // dbp("onPointerUp(): numTouches=%d, timestamp=%d", emEvent.numTouches, emEvent.timestamp); + this->onPointerUp(&releaseEvent, callbackParameter); + } + + this->clear(); + } + + void onTouchCancel(const EmscriptenTouchEvent& emEvent, void* callbackParameter) { + this->onTouchEnd(emEvent, callbackParameter); + } +}; + +static TouchEventHelper touchEventHelper; + +static KeyboardEvent handledKeyboardEvent; + +class WindowImplHtml final : public Window { +public: + std::string emCanvasSel; + EMSCRIPTEN_WEBGL_CONTEXT_HANDLE emContext = 0; + + val htmlContainer; + val htmlEditor; + val scrollbarHelper; + + std::shared_ptr menuBar; + + WindowImplHtml(val htmlContainer, std::string emCanvasSel) : + emCanvasSel(emCanvasSel), + htmlContainer(htmlContainer), + htmlEditor(val::global("document").call("createElement", val("input"))) + { + htmlEditor["classList"].call("add", val("editor")); + htmlEditor["style"].set("display", "none"); + auto editingDoneFunc = [this] { + if(onEditingDone) { + onEditingDone(htmlEditor["value"].as()); + } + }; + RegisterEventListener(htmlEditor, "trigger", editingDoneFunc); + htmlContainer["parentElement"].call("appendChild", htmlEditor); + + std::string scrollbarElementQuery = emCanvasSel + "scrollbar"; + dbp("scrollbar element query: \"%s\"", scrollbarElementQuery.c_str()); + val scrollbarElement = val::global("document").call("querySelector", val(scrollbarElementQuery)); + if (scrollbarElement == val::null()) { + // dbp("scrollbar element is null."); + this->scrollbarHelper = val::null(); + } else { + dbp("scrollbar element OK."); + this->scrollbarHelper = val::global("window")["ScrollbarHelper"].new_(val(scrollbarElementQuery)); + static std::function onScrollCallback = [this] { + // dbp("onScrollCallback std::function this=%p", (void*)this); + if (this->onScrollbarAdjusted) { + double newpos = this->scrollbarHelper.call("getScrollbarPosition"); + // dbp(" call onScrollbarAdjusted(%f)", newpos); + this->onScrollbarAdjusted(newpos); + } + this->Invalidate(); + }; + this->scrollbarHelper.set("onScrollCallback", Wrap(onScrollCallback)); + } + + sscheck(emscripten_set_resize_callback( + EMSCRIPTEN_EVENT_TARGET_WINDOW, this, /*useCapture=*/false, + WindowImplHtml::ResizeCallback)); + sscheck(emscripten_set_resize_callback( + emCanvasSel.c_str(), this, /*useCapture=*/false, + WindowImplHtml::ResizeCallback)); + sscheck(emscripten_set_mousemove_callback( + emCanvasSel.c_str(), this, /*useCapture=*/false, + WindowImplHtml::MouseCallback)); + sscheck(emscripten_set_mousedown_callback( + emCanvasSel.c_str(), this, /*useCapture=*/false, + WindowImplHtml::MouseCallback)); + sscheck(emscripten_set_click_callback( + emCanvasSel.c_str(), this, /*useCapture=*/false, + WindowImplHtml::MouseCallback)); + sscheck(emscripten_set_dblclick_callback( + emCanvasSel.c_str(), this, /*useCapture=*/false, + WindowImplHtml::MouseCallback)); + sscheck(emscripten_set_mouseup_callback( + emCanvasSel.c_str(), this, /*useCapture=*/false, + WindowImplHtml::MouseCallback)); + sscheck(emscripten_set_mouseleave_callback( + emCanvasSel.c_str(), this, /*useCapture=*/false, + WindowImplHtml::MouseCallback)); + + sscheck(emscripten_set_touchstart_callback( + emCanvasSel.c_str(), this, /*useCapture=*/false, + WindowImplHtml::TouchCallback)); + sscheck(emscripten_set_touchmove_callback( + emCanvasSel.c_str(), this, /*useCapture=*/false, + WindowImplHtml::TouchCallback)); + sscheck(emscripten_set_touchend_callback( + emCanvasSel.c_str(), this, /*useCapture=*/false, + WindowImplHtml::TouchCallback)); + sscheck(emscripten_set_touchcancel_callback( + emCanvasSel.c_str(), this, /*useCapture=*/false, + WindowImplHtml::TouchCallback)); + + sscheck(emscripten_set_wheel_callback( + emCanvasSel.c_str(), this, /*useCapture=*/false, + WindowImplHtml::WheelCallback)); + sscheck(emscripten_set_keydown_callback( + EMSCRIPTEN_EVENT_TARGET_WINDOW, this, /*useCapture=*/false, + WindowImplHtml::KeyboardCallback)); + sscheck(emscripten_set_keyup_callback( + EMSCRIPTEN_EVENT_TARGET_WINDOW, this, /*useCapture=*/false, + WindowImplHtml::KeyboardCallback)); + sscheck(emscripten_set_webglcontextlost_callback( + emCanvasSel.c_str(), this, /*useCapture=*/false, + WindowImplHtml::ContextLostCallback)); + sscheck(emscripten_set_webglcontextrestored_callback( + emCanvasSel.c_str(), this, /*useCapture=*/false, + WindowImplHtml::ContextRestoredCallback)); + + ResizeCanvasElement(); + SetupWebGLContext(); + } + + ~WindowImplHtml() { + if(emContext != 0) { + sscheck(emscripten_webgl_destroy_context(emContext)); + } + } + + static EM_BOOL ResizeCallback(int emEventType, const EmscriptenUiEvent *emEvent, void *data) { + WindowImplHtml *window = (WindowImplHtml *)data; + window->Invalidate(); + return EM_TRUE; + } + + static EM_BOOL MouseCallback(int emEventType, const EmscriptenMouseEvent *emEvent, + void *data) { + if(val::global("window").call("isModal")) return EM_FALSE; + + WindowImplHtml *window = (WindowImplHtml *)data; + MouseEvent event = {}; + switch(emEventType) { + case EMSCRIPTEN_EVENT_MOUSEMOVE: + event.type = MouseEvent::Type::MOTION; + break; + case EMSCRIPTEN_EVENT_MOUSEDOWN: + event.type = MouseEvent::Type::PRESS; + break; + case EMSCRIPTEN_EVENT_DBLCLICK: + event.type = MouseEvent::Type::DBL_PRESS; + break; + case EMSCRIPTEN_EVENT_MOUSEUP: + event.type = MouseEvent::Type::RELEASE; + break; + case EMSCRIPTEN_EVENT_MOUSELEAVE: + event.type = MouseEvent::Type::LEAVE; + break; + default: + return EM_FALSE; + } + switch(emEventType) { + case EMSCRIPTEN_EVENT_MOUSEMOVE: + if(emEvent->buttons & 1) { + event.button = MouseEvent::Button::LEFT; + } else if(emEvent->buttons & 2) { + event.button = MouseEvent::Button::RIGHT; + } else if(emEvent->buttons & 4) { + event.button = MouseEvent::Button::MIDDLE; + } + break; + case EMSCRIPTEN_EVENT_MOUSEDOWN: + case EMSCRIPTEN_EVENT_DBLCLICK: + case EMSCRIPTEN_EVENT_MOUSEUP: + switch(emEvent->button) { + case 0: event.button = MouseEvent::Button::LEFT; break; + case 1: event.button = MouseEvent::Button::MIDDLE; break; + case 2: event.button = MouseEvent::Button::RIGHT; break; + } + break; + default: + return EM_FALSE; + } + event.x = emEvent->targetX; + event.y = emEvent->targetY; + event.shiftDown = emEvent->shiftKey || emEvent->altKey; + event.controlDown = emEvent->ctrlKey; + + if(window->onMouseEvent) { + return window->onMouseEvent(event); + } + return EM_FALSE; + } + + static EM_BOOL TouchCallback(int emEventType, const EmscriptenTouchEvent *emEvent, + void *data) { + + if(val::global("window").call("isModal")) return EM_FALSE; + + static bool initialized = false; + + WindowImplHtml *window = (WindowImplHtml *)data; + + if (!initialized) { + touchEventHelper.onPointerDown = [](MouseEvent* event, void* param) -> void { + WindowImplHtml* window = (WindowImplHtml*)param; + if (window->onMouseEvent) { + window->onMouseEvent(*event); + } + }; + touchEventHelper.onPointerMove = [](MouseEvent* event, void* param) -> void { + WindowImplHtml* window = (WindowImplHtml*)param; + if (window->onMouseEvent) { + window->onMouseEvent(*event); + } + }; + touchEventHelper.onPointerUp = [](MouseEvent* event, void* param) -> void { + WindowImplHtml* window = (WindowImplHtml*)param; + if (window->onMouseEvent) { + window->onMouseEvent(*event); + } + }; + touchEventHelper.onScroll = [](MouseEvent* event, void* param) -> void { + WindowImplHtml* window = (WindowImplHtml*)param; + if (window->onMouseEvent) { + window->onMouseEvent(*event); + } + }; + initialized = true; + } + + switch(emEventType) { + case EMSCRIPTEN_EVENT_TOUCHSTART: + touchEventHelper.onTouchStart(*emEvent, window); + break; + case EMSCRIPTEN_EVENT_TOUCHMOVE: + touchEventHelper.onTouchMove(*emEvent, window); + break; + case EMSCRIPTEN_EVENT_TOUCHEND: + touchEventHelper.onTouchEnd(*emEvent, window); + break; + case EMSCRIPTEN_EVENT_TOUCHCANCEL: + touchEventHelper.onTouchCancel(*emEvent, window); + break; + default: + return EM_FALSE; + } + + return true; + } + + static EM_BOOL WheelCallback(int emEventType, const EmscriptenWheelEvent *emEvent, + void *data) { + if(val::global("window").call("isModal")) return EM_FALSE; + + WindowImplHtml *window = (WindowImplHtml *)data; + MouseEvent event = {}; + if(emEvent->deltaY != 0) { + event.type = MouseEvent::Type::SCROLL_VERT; + // FIXME(emscripten): + // Pay attention to: + // dbp("Mouse wheel delta mode: %lu", emEvent->deltaMode); + // https://emscripten.org/docs/api_reference/html5.h.html#id11 + // https://www.w3.org/TR/DOM-Level-3-Events/#dom-wheelevent-deltamode + // and adjust the 0.01 below. deltaMode == 0 on a Firefox on a Windows. + event.scrollDelta = -emEvent->deltaY * 0.01; + } else { + return EM_FALSE; + } + + const EmscriptenMouseEvent &emStatus = emEvent->mouse; + event.x = emStatus.targetX; + event.y = emStatus.targetY; + event.shiftDown = emStatus.shiftKey; + event.controlDown = emStatus.ctrlKey; + + if(window->onMouseEvent) { + return window->onMouseEvent(event); + } + return EM_FALSE; + } + + static EM_BOOL KeyboardCallback(int emEventType, const EmscriptenKeyboardEvent *emEvent, + void *data) { + if(emEvent->altKey) return EM_FALSE; + if(emEvent->repeat) return EM_FALSE; + + WindowImplHtml *window = (WindowImplHtml *)data; + KeyboardEvent event = {}; + switch(emEventType) { + case EMSCRIPTEN_EVENT_KEYDOWN: + event.type = KeyboardEvent::Type::PRESS; + break; + + case EMSCRIPTEN_EVENT_KEYUP: + event.type = KeyboardEvent::Type::RELEASE; + break; + + default: + return EM_FALSE; + } + event.shiftDown = emEvent->shiftKey; + event.controlDown = emEvent->ctrlKey; + + std::string key = emEvent->key; + if(key[0] == 'F' && isdigit(key[1])) { + event.key = KeyboardEvent::Key::FUNCTION; + event.num = std::stol(key.substr(1)); + } else { + event.key = KeyboardEvent::Key::CHARACTER; + + auto utf8 = ReadUTF8(key); + if(++utf8.begin() == utf8.end()) { + event.chr = tolower(*utf8.begin()); + } else if(key == "Escape") { + event.chr = '\e'; + } else if(key == "Tab") { + event.chr = '\t'; + } else if(key == "Backspace") { + event.chr = '\b'; + } else if(key == "Delete") { + event.chr = '\x7f'; + } else { + return EM_FALSE; + } + + if(event.chr == '>' && event.shiftDown) { + event.shiftDown = false; + } + } + + if(event.Equals(handledKeyboardEvent)) return EM_FALSE; + if(val::global("window").call("isModal")) { + handledKeyboardEvent = {}; + return EM_FALSE; + } + + if(window->onKeyboardEvent) { + if(window->onKeyboardEvent(event)) { + handledKeyboardEvent = event; + return EM_TRUE; + } + } + return EM_FALSE; + } + + void SetupWebGLContext() { + EmscriptenWebGLContextAttributes emAttribs = {}; + emscripten_webgl_init_context_attributes(&emAttribs); + emAttribs.alpha = false; + emAttribs.failIfMajorPerformanceCaveat = true; + + sscheck(emContext = emscripten_webgl_create_context(emCanvasSel.c_str(), &emAttribs)); + dbp("Canvas %s: got context %d", emCanvasSel.c_str(), emContext); + } + + static bool ContextLostCallback(int eventType, const void *reserved, void *data) { + WindowImplHtml *window = (WindowImplHtml *)data; + dbp("Canvas %s: context lost", window->emCanvasSel.c_str()); + window->emContext = 0; + + if(window->onContextLost) { + window->onContextLost(); + } + return EM_TRUE; + } + + static bool ContextRestoredCallback(int eventType, const void *reserved, void *data) { + WindowImplHtml *window = (WindowImplHtml *)data; + dbp("Canvas %s: context restored", window->emCanvasSel.c_str()); + window->SetupWebGLContext(); + return EM_TRUE; + } + + void ResizeCanvasElement() { + double width, height; + std::string htmlContainerSel = "#" + htmlContainer["id"].as(); + sscheck(emscripten_get_element_css_size(htmlContainerSel.c_str(), &width, &height)); + // sscheck(emscripten_get_element_css_size(emCanvasSel.c_str(), &width, &height)); + + double devicePixelRatio = GetDevicePixelRatio(); + width *= devicePixelRatio; + height *= devicePixelRatio; + + int currentWidth = 0, currentHeight = 0; + sscheck(emscripten_get_canvas_element_size(emCanvasSel.c_str(), ¤tWidth, ¤tHeight)); + + if ((int)width != currentWidth || (int)height != currentHeight) { + // dbp("Canvas %s container current size: (%d, %d)", emCanvasSel.c_str(), (int)currentWidth, (int)currentHeight); + // dbp("Canvas %s: resizing to (%d, %d)", emCanvasSel.c_str(), (int)width, (int)height); + sscheck(emscripten_set_canvas_element_size(emCanvasSel.c_str(), (int)width, (int)height)); + } + } + + static void RenderCallback(void *data) { + WindowImplHtml *window = (WindowImplHtml *)data; + if(window->emContext == 0) { + dbp("Canvas %s: cannot render: no context", window->emCanvasSel.c_str()); + return; + } + + window->ResizeCanvasElement(); + sscheck(emscripten_webgl_make_context_current(window->emContext)); + if(window->onRender) { + window->onRender(); + } + } + + double GetPixelDensity() override { + return 96.0 * GetDevicePixelRatio(); + } + + double GetDevicePixelRatio() override { + return emscripten_get_device_pixel_ratio(); + } + + bool IsVisible() override { + // FIXME(emscripten): implement + return true; + } + + void SetVisible(bool visible) override { + // FIXME(emscripten): implement + } + + void Focus() override { + // Do nothing, we can't affect focus of browser windows. + } + + bool IsFullScreen() override { + EmscriptenFullscreenChangeEvent emEvent = {}; + sscheck(emscripten_get_fullscreen_status(&emEvent)); + return emEvent.isFullscreen; + } + + void SetFullScreen(bool fullScreen) override { + if(fullScreen) { + EmscriptenFullscreenStrategy emStrategy = {}; + emStrategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH; + emStrategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_HIDEF; + emStrategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; + sscheck(emscripten_request_fullscreen_strategy( + emCanvasSel.c_str(), /*deferUntilInEventHandler=*/true, &emStrategy)); + } else { + sscheck(emscripten_exit_fullscreen()); + } + } + + void SetTitle(const std::string &title) override { + // FIXME(emscripten): implement + } + + void SetMenuBar(MenuBarRef menuBar) override { + std::shared_ptr menuBarImpl = + std::static_pointer_cast(menuBar); + this->menuBar = menuBarImpl; + + val htmlMain = val::global("document").call("querySelector", val("main")); + val htmlCurrentMenuBar = htmlMain.call("querySelector", val(".menubar")); + if(htmlCurrentMenuBar.as()) { + htmlCurrentMenuBar.call("remove"); + } + htmlMain.call("insertBefore", menuBarImpl->htmlMenuBar, + htmlMain["firstChild"]); + ResizeCanvasElement(); + } + + void GetContentSize(double *width, double *height) override { + sscheck(emscripten_get_element_css_size(emCanvasSel.c_str(), width, height)); + } + + void SetMinContentSize(double width, double height) override { + // Do nothing, we can't affect sizing of browser windows. + } + + void FreezePosition(SettingsRef settings, const std::string &key) override { + // Do nothing, we can't position browser windows. + } + + void ThawPosition(SettingsRef settings, const std::string &key) override { + // Do nothing, we can't position browser windows. + } + + void SetCursor(Cursor cursor) override { + std::string htmlCursor; + switch(cursor) { + case Cursor::POINTER: htmlCursor = "default"; break; + case Cursor::HAND: htmlCursor = "pointer"; break; + } + htmlContainer["style"].set("cursor", htmlCursor); + } + + void SetTooltip(const std::string &text, double x, double y, + double width, double height) override { + val htmlCanvas = + val::global("document").call("querySelector", emCanvasSel); + htmlCanvas.set("title", text); + } + + bool IsEditorVisible() override { + return htmlEditor["style"]["display"].as() != "none"; + } + + void ShowEditor(double x, double y, double fontHeight, double minWidth, + bool isMonospace, const std::string &text) override { + htmlEditor["style"].set("display", val("")); + val canvasClientRect = val::global("document").call("querySelector", val(this->emCanvasSel)).call("getBoundingClientRect"); + double canvasLeft = canvasClientRect["left"].as(); + double canvasTop = canvasClientRect["top"].as(); + htmlEditor["style"].set("left", std::to_string(canvasLeft + x - 4) + "px"); + htmlEditor["style"].set("top", std::to_string(canvasTop + y - fontHeight - 2) + "px"); + htmlEditor["style"].set("fontSize", std::to_string(fontHeight) + "px"); + htmlEditor["style"].set("minWidth", std::to_string(minWidth) + "px"); + htmlEditor["style"].set("fontFamily", isMonospace ? "monospace" : "sans"); + htmlEditor.set("value", text); + htmlEditor.call("focus"); + } + + void HideEditor() override { + htmlEditor["style"].set("display", val("none")); + } + + void SetScrollbarVisible(bool visible) override { + // dbp("SetScrollbarVisible(): visible=%d", visible ? 1 : 0); + if (this->scrollbarHelper == val::null()) { + // dbp("scrollbarHelper is null."); + return; + } + if (!visible) { + this->scrollbarHelper.call("setScrollbarEnabled", val(false)); + } + } + + double scrollbarPos = 0.0; + double scrollbarMin = 0.0; + double scrollbarMax = 0.0; + double scrollbarPageSize = 0.0; + + void ConfigureScrollbar(double min, double max, double pageSize) override { + // dbp("ConfigureScrollbar(): min=%f, max=%f, pageSize=%f", min, max, pageSize); + if (this->scrollbarHelper == val::null()) { + // dbp("scrollbarHelper is null."); + return; + } + // FIXME(emscripten): implement + this->scrollbarMin = min; + this->scrollbarMax = max; + this->scrollbarPageSize = pageSize; + + this->scrollbarHelper.call("setRange", this->scrollbarMin, this->scrollbarMax); + this->scrollbarHelper.call("setPageSize", pageSize); + } + + double GetScrollbarPosition() override { + // dbp("GetScrollbarPosition()"); + if (this->scrollbarHelper == val::null()) { + // dbp("scrollbarHelper is null."); + return 0; + } + this->scrollbarPos = this->scrollbarHelper.call("getScrollbarPosition"); + // dbp(" GetScrollbarPosition() returns %f", this->scrollbarPos); + return scrollbarPos; + } + + void SetScrollbarPosition(double pos) override { + // dbp("SetScrollbarPosition(): pos=%f", pos); + if (this->scrollbarHelper == val::null()) { + // dbp("scrollbarHelper is null."); + return; + } + this->scrollbarHelper.call("setScrollbarPosition", pos); + scrollbarPos = pos; + } + + void Invalidate() override { + emscripten_async_call(WindowImplHtml::RenderCallback, this, -1); + } +}; + +WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { + static int windowNum; + + std::string htmlContainerId = std::string("container") + std::to_string(windowNum); + val htmlContainer = + val::global("document").call("getElementById", htmlContainerId); + std::string emCanvasSel = std::string("#canvas") + std::to_string(windowNum); + + windowNum++; + return std::make_shared(htmlContainer, emCanvasSel); +} + +//----------------------------------------------------------------------------- +// 3DConnexion support +//----------------------------------------------------------------------------- + +void Open3DConnexion() {} +void Close3DConnexion() {} +void Request3DConnexionEventsForWindow(WindowRef window) {} + +//----------------------------------------------------------------------------- +// Message dialogs +//----------------------------------------------------------------------------- + +class MessageDialogImplHtml; + +static std::vector> dialogsOnScreen; + +class MessageDialogImplHtml final : public MessageDialog, + public std::enable_shared_from_this { +public: + val htmlModal; + val htmlDialog; + val htmlMessage; + val htmlDescription; + val htmlButtons; + + bool is_shown = false; + + Response latestResponse = Response::NONE; + + MessageDialogImplHtml() : + htmlModal(val::global("document").call("createElement", val("div"))), + htmlDialog(val::global("document").call("createElement", val("div"))), + htmlMessage(val::global("document").call("createElement", val("strong"))), + htmlDescription(val::global("document").call("createElement", val("p"))), + htmlButtons(val::global("document").call("createElement", val("div"))) + { + htmlModal["classList"].call("add", val("modal")); + htmlModal.call("appendChild", htmlDialog); + htmlDialog["classList"].call("add", val("dialog")); + htmlDialog.call("appendChild", htmlMessage); + htmlDialog.call("appendChild", htmlDescription); + htmlButtons["classList"].call("add", val("buttons")); + htmlDialog.call("appendChild", htmlButtons); + } + + void SetType(Type type) { + // FIXME(emscripten): implement + } + + void SetTitle(std::string title) { + // FIXME(emscripten): implement + } + + void SetMessage(std::string message) { + htmlMessage.set("innerText", message); + } + + void SetDescription(std::string description) { + htmlDescription.set("innerText", description); + } + + void AddButton(std::string label, Response response, bool isDefault = false) { + val htmlButton = val::global("document").call("createElement", val("div")); + htmlButton["classList"].call("add", val("button")); + val::global("window").call("setLabelWithMnemonic", htmlButton, label); + if(isDefault) { + htmlButton["classList"].call("add", val("default"), val("selected")); + } + + std::function responseFunc = [this, response] { + htmlModal.call("remove"); + this->latestResponse = response; + if(onResponse) { + onResponse(response); + } + auto it = std::remove(dialogsOnScreen.begin(), dialogsOnScreen.end(), + shared_from_this()); + dialogsOnScreen.erase(it); + + this->is_shown = false; + }; + RegisterEventListener(htmlButton, "trigger", responseFunc); + + htmlButtons.call("appendChild", htmlButton); + } + + Response RunModal() { + this->ShowModal(); + //FIXME(emscripten): use val::await() with JavaScript's Promise + while (true) { + if (this->is_shown) { + emscripten_sleep(50); + } else { + break; + } + } + + if (this->latestResponse != Response::NONE) { + return this->latestResponse; + } else { + // FIXME(emscripten): + return this->latestResponse; + } + } + + void ShowModal() { + dialogsOnScreen.push_back(shared_from_this()); + val::global("document")["body"].call("appendChild", htmlModal); + + this->is_shown = true; + } +}; + +MessageDialogRef CreateMessageDialog(WindowRef parentWindow) { + return std::make_shared(); +} + +//----------------------------------------------------------------------------- +// File dialogs +//----------------------------------------------------------------------------- + +// In emscripten pseudo filesystem, all userdata will be stored in this directory. +static std::string basePathInFilesystem = "/data/"; + + +/* FileDialog that can open, save and browse. Also refer `src/platform/html/filemanagerui.js`. + */ +class FileDialogImplHtml : public FileDialog { +public: + + enum class Modes { + OPEN = 0, + SAVE, + BROWSER + }; + + Modes mode; + + std::string title; + std::string filename; + std::string filters; + + val jsFileManagerUI; + + FileDialogImplHtml(Modes mode) { + dbp("FileDialogImplHtml::FileDialogImplHtml()"); + val fileManagerUIClass = val::global("window")["FileManagerUI"]; + val dialogModeValue; + this->mode = mode; + if (mode == Modes::OPEN) { + dialogModeValue = val(0); + } else if (mode == Modes::SAVE) { + dialogModeValue = val(1); + } else { + dialogModeValue = val(2); + } + this->jsFileManagerUI = fileManagerUIClass.new_(dialogModeValue); + dbp("FileDialogImplHtml::FileDialogImplHtml() Done."); + } + + ~FileDialogImplHtml() override { + dbp("FileDialogImplHtml::~FileDialogImplHtml()"); + this->jsFileManagerUI.call("dispose"); + } + + void SetTitle(std::string title) override { + dbp("FileDialogImplHtml::SetTitle(): title=\"%s\"", title.c_str()); + this->title = title; + this->jsFileManagerUI.call("setTitle", val(title)); + } + + void SetCurrentName(std::string name) override { + dbp("FileDialogImplHtml::SetCurrentName(): name=\"%s\", parent=\"%s\"", name.c_str(), this->GetFilename().Parent().raw.c_str()); + + Path filepath = Path::From(name); + if (filepath.IsAbsolute()) { + // dbp("FileDialogImplHtml::SetCurrentName(): path is absolute."); + SetFilename(filepath); + } else { + // dbp("FileDialogImplHtml::SetCurrentName(): path is relative."); + SetFilename(GetFilename().Parent().Join(name)); + } + } + + Platform::Path GetFilename() override { + return Platform::Path::From(this->filename.c_str()); + } + + void SetFilename(Platform::Path path) override { + dbp("FileDialogImplHtml::GetFilename(): path=\"%s\"", path.raw.c_str()); + this->filename = std::string(path.raw); + std::string filename_ = Path::From(this->filename).FileName(); + this->jsFileManagerUI.call("setDefaultFilename", val(filename_)); + } + + void SuggestFilename(Platform::Path path) override { + dbp("FileDialogImplHtml::SuggestFilename(): path=\"%s\"", path.raw.c_str()); + SetFilename(Platform::Path::From(path.FileStem())); + } + + void AddFilter(std::string name, std::vector extensions) override { + if (this->filters.length() > 0) { + this->filters += ","; + } + for (size_t i = 0; i < extensions.size(); i++) { + if (i != 0) { + this->filters += ","; + } + this->filters += "." + extensions[i]; + } + dbp("FileDialogImplHtml::AddFilter(): filter=%s", this->filters.c_str()); + this->jsFileManagerUI.call("setFilter", val(this->filters)); + } + + void FreezeChoices(SettingsRef settings, const std::string &key) override { + //FIXME(emscripten): implement + } + + void ThawChoices(SettingsRef settings, const std::string &key) override { + //FIXME(emscripten): implement + } + + bool RunModal() override { + dbp("FileDialogImplHtml::RunModal()"); + + this->jsFileManagerUI.call("setBasePath", val(basePathInFilesystem)); + this->jsFileManagerUI.call("show"); + while (true) { + bool isShown = this->jsFileManagerUI.call("isShown"); + if (!isShown) { + break; + } else { + emscripten_sleep(50); + } + } + + dbp("FileSaveDialogImplHtml::RunModal() : dialog closed."); + + std::string selectedFilename = this->jsFileManagerUI.call("getSelectedFilename"); + if (selectedFilename.length() > 0) { + // Dummy call to set parent directory + this->SetFilename(Path::From(basePathInFilesystem + "/dummy")); + this->SetCurrentName(selectedFilename); + } + + + if (selectedFilename.length() > 0) { + return true; + } else { + return false; + } + } +}; + +FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) { + dbp("CreateOpenFileDialog()"); + return std::shared_ptr(new FileDialogImplHtml(FileDialogImplHtml::Modes::OPEN)); +} + +FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) { + dbp("CreateSaveFileDialog()"); + return std::shared_ptr(new FileDialogImplHtml(FileDialogImplHtml::Modes::SAVE)); +} + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +std::vector GetFontFiles() { + return {}; +} + +void OpenInBrowser(const std::string &url) { + val::global("window").call("open", url); +} + + +void OnSaveFinishedCallback(const Platform::Path& filename, bool is_saveAs, bool is_autosave) { + dbp("OnSaveFinished(): %s, is_saveAs=%d, is_autosave=%d\n", filename.FileName().c_str(), is_saveAs, is_autosave); + val::global("window").call("saveFileDone", filename.raw, is_saveAs, is_autosave); +} + +std::vector InitGui(int argc, char **argv) { + std::function onBeforeUnload = std::bind(&SolveSpaceUI::Exit, &SS); + RegisterEventListener(val::global("window"), "beforeunload", onBeforeUnload); + + // dbp("Set onSaveFinished"); + SS.OnSaveFinished = OnSaveFinishedCallback; + + // FIXME(emscripten): get locale from user preferences + SetLocale("en_US"); + + return {}; +} + +static void MainLoopIteration() { + // We don't do anything here, as all our work is registered via timers. +} + +void RunGui() { + emscripten_set_main_loop(MainLoopIteration, 0, /*simulate_infinite_loop=*/true); +} + +void ExitGui() { + exit(0); +} + +void ClearGui() {} + +} +} diff --git a/src/platform/guimac.mm b/src/platform/guimac.mm new file mode 100644 index 0000000..8f25afb --- /dev/null +++ b/src/platform/guimac.mm @@ -0,0 +1,1688 @@ +//----------------------------------------------------------------------------- +// The Cocoa-based implementation of platform-dependent GUI functionality. +// +// Copyright 2018 whitequark +//----------------------------------------------------------------------------- +#include "solvespace.h" +#import + +using namespace SolveSpace; + +//----------------------------------------------------------------------------- +// Internal AppKit classes +//----------------------------------------------------------------------------- + +@interface NSToolTipManager : NSObject ++ (NSToolTipManager *)sharedToolTipManager; +- (void)setInitialToolTipDelay:(double)delay; +- (void)orderOutToolTip; +- (void)abortToolTip; +- (void)_displayTemporaryToolTipForView:(id)arg1 withString:(id)arg2; +@end + +//----------------------------------------------------------------------------- +// Objective-C bridging +//----------------------------------------------------------------------------- + +static NSString* Wrap(const std::string &s) { + return [NSString stringWithUTF8String:s.c_str()]; +} + +@interface SSFunction : NSObject +- (SSFunction *)initWithFunction:(std::function *)aFunc; +- (void)run; +@end + +@implementation SSFunction +{ + std::function *func; +} + +- (SSFunction *)initWithFunction:(std::function *)aFunc { + if(self = [super init]) { + func = aFunc; + } + return self; +} + +- (void)run { + if(*func) (*func)(); +} +@end + +namespace SolveSpace { +namespace Platform { + +//----------------------------------------------------------------------------- +// Utility functions +//----------------------------------------------------------------------------- + +static std::string PrepareMnemonics(std::string label) { + // OS X does not support mnemonics + label.erase(std::remove(label.begin(), label.end(), '&'), label.end()); + return label; +} + +//----------------------------------------------------------------------------- +// Fatal errors +//----------------------------------------------------------------------------- + +// This gets put into the "Application Specific Information" field in crash +// reporter dialog. +typedef struct { + unsigned version __attribute__((aligned(8))); + const char *message __attribute__((aligned(8))); + const char *signature __attribute__((aligned(8))); + const char *backtrace __attribute__((aligned(8))); + const char *message2 __attribute__((aligned(8))); + void *reserved __attribute__((aligned(8))); + void *reserved2 __attribute__((aligned(8))); +} crash_info_t; + +#define CRASH_VERSION 4 + +crash_info_t crashAnnotation __attribute__((section("__DATA,__crash_info"))) = { + CRASH_VERSION, NULL, NULL, NULL, NULL, NULL, NULL +}; + +void FatalError(const std::string &message) { + crashAnnotation.message = message.c_str(); + abort(); +} + +//----------------------------------------------------------------------------- +// Settings +//----------------------------------------------------------------------------- + +class SettingsImplCocoa final : public Settings { +public: + NSUserDefaults *userDefaults; + + SettingsImplCocoa() { + userDefaults = [NSUserDefaults standardUserDefaults]; + } + + void FreezeInt(const std::string &key, uint32_t value) override { + [userDefaults setInteger:value forKey:Wrap(key)]; + } + + uint32_t ThawInt(const std::string &key, uint32_t defaultValue = 0) override { + NSString *nsKey = Wrap(key); + if([userDefaults objectForKey:nsKey]) { + return [userDefaults integerForKey:nsKey]; + } + return defaultValue; + } + + void FreezeBool(const std::string &key, bool value) override { + [userDefaults setBool:value forKey:Wrap(key)]; + } + + bool ThawBool(const std::string &key, bool defaultValue = false) override { + NSString *nsKey = Wrap(key); + if([userDefaults objectForKey:nsKey]) { + return [userDefaults boolForKey:nsKey]; + } + return defaultValue; + } + + void FreezeFloat(const std::string &key, double value) override { + [userDefaults setDouble:value forKey:Wrap(key)]; + } + + double ThawFloat(const std::string &key, double defaultValue = 0.0) override { + NSString *nsKey = Wrap(key); + if([userDefaults objectForKey:nsKey]) { + return [userDefaults doubleForKey:nsKey]; + } + return defaultValue; + } + + void FreezeString(const std::string &key, const std::string &value) override { + [userDefaults setObject:Wrap(value) forKey:Wrap(key)]; + } + + std::string ThawString(const std::string &key, + const std::string &defaultValue = "") override { + NSObject *nsValue = [userDefaults objectForKey:Wrap(key)]; + if(nsValue && [nsValue isKindOfClass:[NSString class]]) { + return [(NSString *)nsValue UTF8String]; + } + return defaultValue; + } +}; + +SettingsRef GetSettings() { + return std::make_shared(); +} + +//----------------------------------------------------------------------------- +// Timers +//----------------------------------------------------------------------------- + +class TimerImplCocoa final : public Timer { +public: + NSTimer *timer; + + TimerImplCocoa() : timer(NULL) {} + + void RunAfter(unsigned milliseconds) override { + SSFunction *callback = [[SSFunction alloc] initWithFunction:&this->onTimeout]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature: + [callback methodSignatureForSelector:@selector(run)]]; + invocation.target = callback; + invocation.selector = @selector(run); + + if(timer != NULL) { + [timer invalidate]; + } + timer = [NSTimer timerWithTimeInterval:(milliseconds / 1000.0) + invocation:invocation repeats:NO]; + [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; + } + + ~TimerImplCocoa() { + if(timer != NULL) { + [timer invalidate]; + } + } +}; + +TimerRef CreateTimer() { + return std::make_shared(); +} + +//----------------------------------------------------------------------------- +// Menus +//----------------------------------------------------------------------------- + +class MenuItemImplCocoa final : public MenuItem { +public: + SSFunction *ssFunction; + NSMenuItem *nsMenuItem; + + MenuItemImplCocoa() { + ssFunction = [[SSFunction alloc] initWithFunction:&onTrigger]; + nsMenuItem = [[NSMenuItem alloc] initWithTitle:@"" + action:@selector(run) keyEquivalent:@""]; + nsMenuItem.target = ssFunction; + } + + void SetAccelerator(KeyboardEvent accel) override { + unichar accelChar; + switch(accel.key) { + case KeyboardEvent::Key::CHARACTER: + if(accel.chr == NSDeleteCharacter) { + accelChar = NSBackspaceCharacter; + } else { + accelChar = accel.chr; + } + break; + + case KeyboardEvent::Key::FUNCTION: + accelChar = NSF1FunctionKey + accel.num - 1; + break; + } + nsMenuItem.keyEquivalent = [[NSString alloc] initWithCharacters:&accelChar length:1]; + + NSUInteger modifierMask = 0; + if(accel.shiftDown) + modifierMask |= NSEventModifierFlagShift; + if(accel.controlDown) + modifierMask |= NSEventModifierFlagCommand; + nsMenuItem.keyEquivalentModifierMask = modifierMask; + } + + void SetIndicator(Indicator state) override { + // macOS does not support radio menu items + } + + void SetActive(bool active) override { + nsMenuItem.state = active ? NSControlStateValueOn : NSControlStateValueOff; + } + + void SetEnabled(bool enabled) override { + nsMenuItem.enabled = enabled; + } +}; + +class MenuImplCocoa final : public Menu { +public: + NSMenu *nsMenu; + + std::vector> menuItems; + std::vector> subMenus; + + MenuImplCocoa() { + nsMenu = [[NSMenu alloc] initWithTitle:@""]; + [nsMenu setAutoenablesItems:NO]; + } + + MenuItemRef AddItem(const std::string &label, + std::function onTrigger = NULL, + bool mnemonics = true) override { + auto menuItem = std::make_shared(); + menuItems.push_back(menuItem); + + menuItem->onTrigger = onTrigger; + [menuItem->nsMenuItem setTitle:Wrap(mnemonics ? PrepareMnemonics(label) : label)]; + [nsMenu addItem:menuItem->nsMenuItem]; + + return menuItem; + } + + MenuRef AddSubMenu(const std::string &label) override { + auto subMenu = std::make_shared(); + subMenus.push_back(subMenu); + + NSMenuItem *nsMenuItem = + [nsMenu addItemWithTitle:Wrap(PrepareMnemonics(label)) action:nil keyEquivalent:@""]; + [nsMenu setSubmenu:subMenu->nsMenu forItem:nsMenuItem]; + + return subMenu; + } + + void AddSeparator() override { + [nsMenu addItem:[NSMenuItem separatorItem]]; + } + + void PopUp() override { + NSEvent* event = [NSApp currentEvent]; + [NSMenu popUpContextMenu:nsMenu withEvent:event forView:event.window.contentView]; + } + + void Clear() override { + [nsMenu removeAllItems]; + menuItems.clear(); + subMenus.clear(); + } +}; + +MenuRef CreateMenu() { + return std::make_shared(); +} + +class MenuBarImplCocoa final : public MenuBar { +public: + NSMenu *nsMenuBar; + + std::vector> subMenus; + + MenuBarImplCocoa() { + nsMenuBar = [NSApp mainMenu]; + } + + MenuRef AddSubMenu(const std::string &label) override { + auto subMenu = std::make_shared(); + subMenus.push_back(subMenu); + + NSMenuItem *nsMenuItem = [nsMenuBar addItemWithTitle:@"" action:nil keyEquivalent:@""]; + [subMenu->nsMenu setTitle:Wrap(PrepareMnemonics(label))]; + [nsMenuBar setSubmenu:subMenu->nsMenu forItem:nsMenuItem]; + + return subMenu; + } + + void Clear() override { + while([nsMenuBar numberOfItems] != 1) { + [nsMenuBar removeItemAtIndex:1]; + } + subMenus.clear(); + } +}; + +MenuBarRef GetOrCreateMainMenu(bool *unique) { + static std::shared_ptr mainMenu; + if(!mainMenu) { + mainMenu = std::make_shared(); + } + *unique = true; + return mainMenu; +} + +} +} + +//----------------------------------------------------------------------------- +// Cocoa NSView and NSWindow extensions +//----------------------------------------------------------------------------- + +@interface SSView : NSOpenGLView +@property Platform::Window *receiver; + +@property BOOL acceptsFirstResponder; + +@property(readonly, getter=isEditing) BOOL editing; +- (void)startEditing:(NSString *)text at:(NSPoint)origin + withHeight:(double)fontHeight minWidth:(double)minWidth + usingMonospace:(BOOL)isMonospace; +- (void)stopEditing; +- (void)didEdit:(NSString *)text; + +@property double scrollerMin; +@property double scrollerSize; +@property double pageSize; + +@end + +@implementation SSView +{ + NSTrackingArea *trackingArea; + NSTextField *editor; + double magnificationGestureCurrentZ; + double rotationGestureCurrent; + Point2d trackpadPositionShift; + bool inTrackpadScrollGesture; + int activeTrackpadTouches; + bool scrollFromTrackpadTouch; + Platform::Window::Kind kind; +} + +@synthesize acceptsFirstResponder; + +- (id)initWithKind:(Platform::Window::Kind)aKind { + NSOpenGLPixelFormatAttribute attrs[] = { + NSOpenGLPFADoubleBuffer, + NSOpenGLPFAColorSize, 24, + NSOpenGLPFADepthSize, 24, + 0 + }; + NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs]; + if(self = [super initWithFrame:NSMakeRect(0, 0, 0, 0) pixelFormat:pixelFormat]) { + self.wantsBestResolutionOpenGLSurface = YES; + self.wantsLayer = YES; + editor = [[NSTextField alloc] init]; + editor.editable = YES; + [[editor cell] setWraps:NO]; + [[editor cell] setScrollable:YES]; + editor.bezeled = NO; + editor.target = self; + editor.action = @selector(didEdit:); + + inTrackpadScrollGesture = false; + activeTrackpadTouches = 0; + scrollFromTrackpadTouch = false; + self.acceptsTouchEvents = YES; + kind = aKind; + if(kind == Platform::Window::Kind::TOPLEVEL) { + NSGestureRecognizer *mag = [[NSMagnificationGestureRecognizer alloc] initWithTarget:self + action:@selector(magnifyGesture:)]; + [self addGestureRecognizer:mag]; + + NSRotationGestureRecognizer* rot = [[NSRotationGestureRecognizer alloc] initWithTarget:self + action:@selector(rotateGesture:)]; + [self addGestureRecognizer:rot]; + } + } + return self; +} + +- (void)dealloc { +} + +- (BOOL)isFlipped { + return YES; +} + +@synthesize receiver; + +- (void)drawRect:(NSRect)aRect { + [[self openGLContext] makeCurrentContext]; + if(receiver->onRender) { + receiver->onRender(); + } + [[self openGLContext] flushBuffer]; +} + +- (BOOL)acceptsFirstMouse:(NSEvent *)event { + return YES; +} + +- (void)updateTrackingAreas { + [self removeTrackingArea:trackingArea]; + trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] + options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | + ([self acceptsFirstResponder] + ? NSTrackingActiveInKeyWindow + : NSTrackingActiveAlways)) + owner:self userInfo:nil]; + [self addTrackingArea:trackingArea]; + [super updateTrackingAreas]; +} + +- (Platform::MouseEvent)convertMouseEvent:(NSEvent *)nsEvent { + Platform::MouseEvent event = {}; + + NSPoint nsPoint = [self convertPoint:nsEvent.locationInWindow fromView:nil]; + event.x = nsPoint.x; + event.y = nsPoint.y; + + NSUInteger nsFlags = [nsEvent modifierFlags]; + if(nsFlags & NSEventModifierFlagShift) event.shiftDown = true; + if(nsFlags & NSEventModifierFlagCommand) event.controlDown = true; + + return event; +} + +- (void)mouseMotionEvent:(NSEvent *)nsEvent { + using Platform::MouseEvent; + + MouseEvent event = [self convertMouseEvent:nsEvent]; + event.type = MouseEvent::Type::MOTION; + event.button = MouseEvent::Button::NONE; + + NSUInteger nsButtons = [NSEvent pressedMouseButtons]; + if(nsButtons & (1 << 0)) { + event.button = MouseEvent::Button::LEFT; + } else if(nsButtons & (1 << 1)) { + event.button = MouseEvent::Button::RIGHT; + } else if(nsButtons & (1 << 2)) { + event.button = MouseEvent::Button::MIDDLE; + } + + if(receiver->onMouseEvent) { + receiver->onMouseEvent(event); + } +} + +- (void)mouseMotionEvent:(NSEvent *)nsEvent withButton:(Platform::MouseEvent::Button)button { + using Platform::MouseEvent; + + MouseEvent event = [self convertMouseEvent:nsEvent]; + event.type = MouseEvent::Type::MOTION; + event.button = button; + + if(receiver->onMouseEvent) { + receiver->onMouseEvent(event); + } +} + +- (void)mouseMoved:(NSEvent *)nsEvent { + [self mouseMotionEvent:nsEvent]; + [super mouseMoved:nsEvent]; +} + +- (void)mouseDragged:(NSEvent *)nsEvent { + [self mouseMotionEvent:nsEvent withButton:Platform::MouseEvent::Button::LEFT]; +} + +- (void)otherMouseDragged:(NSEvent *)nsEvent { + [self mouseMotionEvent:nsEvent]; +} + +- (void)rightMouseDragged:(NSEvent *)nsEvent { + [self mouseMotionEvent:nsEvent]; +} + +- (void)mouseButtonEvent:(NSEvent *)nsEvent withType:(Platform::MouseEvent::Type)type { + using Platform::MouseEvent; + + MouseEvent event = [self convertMouseEvent:nsEvent]; + event.type = type; + if([nsEvent buttonNumber] == 0) { + event.button = MouseEvent::Button::LEFT; + } else if([nsEvent buttonNumber] == 1) { + event.button = MouseEvent::Button::RIGHT; + } else if([nsEvent buttonNumber] == 2) { + event.button = MouseEvent::Button::MIDDLE; + } else return; + + if(receiver->onMouseEvent) { + receiver->onMouseEvent(event); + } +} + +- (void)mouseDownEvent:(NSEvent *)nsEvent { + using Platform::MouseEvent; + + MouseEvent::Type type; + if([nsEvent clickCount] == 1) { + type = MouseEvent::Type::PRESS; + } else { + type = MouseEvent::Type::DBL_PRESS; + } + [self mouseButtonEvent:nsEvent withType:type]; +} + +- (void)mouseUpEvent:(NSEvent *)nsEvent { + using Platform::MouseEvent; + + [self mouseButtonEvent:nsEvent withType:(MouseEvent::Type::RELEASE)]; +} + +- (void)mouseDown:(NSEvent *)nsEvent { + [self mouseDownEvent:nsEvent]; +} + +- (void)otherMouseDown:(NSEvent *)nsEvent { + [self mouseDownEvent:nsEvent]; +} + +- (void)rightMouseDown:(NSEvent *)nsEvent { + [self mouseDownEvent:nsEvent]; + [super rightMouseDown:nsEvent]; +} + +- (void)mouseUp:(NSEvent *)nsEvent { + [self mouseUpEvent:nsEvent]; +} + +- (void)otherMouseUp:(NSEvent *)nsEvent { + [self mouseUpEvent:nsEvent]; +} + +- (void)rightMouseUp:(NSEvent *)nsEvent { + [self mouseUpEvent:nsEvent]; +} + +- (void)scrollWheel:(NSEvent *)nsEvent { + using Platform::MouseEvent; + + MouseEvent event = [self convertMouseEvent:nsEvent]; + if(nsEvent.phase == NSEventPhaseBegan) { + // If this scroll began on trackpad then touchesBeganWithEvent was called prior to this + // event and we have at least one active trackpad touch. We store this information so we + // can handle scroll originating from trackpad differently below. + scrollFromTrackpadTouch = activeTrackpadTouches > 0 && + nsEvent.subtype == NSEventSubtypeTabletPoint && + kind == Platform::Window::Kind::TOPLEVEL; + } + // Check if we are scrolling on trackpad and handle things differently. + if(scrollFromTrackpadTouch) { + // This is how Cocoa represents 2 finger trackpad drag gestures, rather than going via + // NSPanGestureRecognizer which is how you might expect this to work... We complicate this + // further by also handling shift-two-finger-drag to mean rotate. Fortunately we're using + // shift in the same way as right-mouse-button MouseEvent does (to converts a pan to a + // rotate) so we get the rotate support for free. It's a bit ugly having to fake mouse + // events and track the deviation from the actual mouse cursor with trackpadPositionShift, + // but in lieu of an event API that allows us to request a rotate/pan with relative + // coordinates, it's the best we can do. + event.button = MouseEvent::Button::RIGHT; + // Make sure control (actually cmd) isn't passed through, ctrl-right-click-drag has special + // meaning as rotate which we don't want to inadvertently trigger. + event.controlDown = false; + if(nsEvent.scrollingDeltaX == 0 && nsEvent.scrollingDeltaY == 0) { + // Cocoa represents the point where the user lifts their fingers off (and any inertial + // scrolling has finished) by an event with scrollingDeltaX and scrollingDeltaY both 0. + // Sometimes you also get a zero scroll at the start of a two-finger-rotate (probably + // reflecting the internal implementation of that being a cancelled possible pan + // gesture), which is why this conditional is structured the way it is. + if(inTrackpadScrollGesture) { + event.x += trackpadPositionShift.x; + event.y += trackpadPositionShift.y; + event.type = MouseEvent::Type::RELEASE; + receiver->onMouseEvent(event); + inTrackpadScrollGesture = false; + trackpadPositionShift = Point2d::From(0, 0); + } + return; + } else if(!inTrackpadScrollGesture) { + inTrackpadScrollGesture = true; + trackpadPositionShift = Point2d::From(0, 0); + event.type = MouseEvent::Type::PRESS; + receiver->onMouseEvent(event); + // And drop through + } + + trackpadPositionShift.x += nsEvent.scrollingDeltaX; + trackpadPositionShift.y += nsEvent.scrollingDeltaY; + event.type = MouseEvent::Type::MOTION; + event.x += trackpadPositionShift.x; + event.y += trackpadPositionShift.y; + receiver->onMouseEvent(event); + return; + } + + event.type = MouseEvent::Type::SCROLL_VERT; + + bool isPrecise = [nsEvent hasPreciseScrollingDeltas]; + event.scrollDelta = [nsEvent scrollingDeltaY] / (isPrecise ? 50 : 5); + + receiver->onMouseEvent(event); +} + +- (void)touchesBeganWithEvent:(NSEvent *)event { + activeTrackpadTouches++; +} + +- (void)touchesEndedWithEvent:(NSEvent *)event { + activeTrackpadTouches--; +} + +- (void)touchesCancelledWithEvent:(NSEvent *)event { + activeTrackpadTouches--; +} + +- (void)mouseExited:(NSEvent *)nsEvent { + using Platform::MouseEvent; + + MouseEvent event = [self convertMouseEvent:nsEvent]; + event.type = MouseEvent::Type::LEAVE; + + if(receiver->onMouseEvent) { + receiver->onMouseEvent(event); + } +} + +- (Platform::KeyboardEvent)convertKeyboardEvent:(NSEvent *)nsEvent { + using Platform::KeyboardEvent; + + KeyboardEvent event = {}; + + NSUInteger nsFlags = [nsEvent modifierFlags]; + if(nsFlags & NSEventModifierFlagShift) + event.shiftDown = true; + if(nsFlags & NSEventModifierFlagCommand) + event.controlDown = true; + + unichar chr = 0; + if(NSString *nsChr = [[nsEvent charactersIgnoringModifiers] lowercaseString]) { + chr = [nsChr characterAtIndex:0]; + } + if(chr >= NSF1FunctionKey && chr <= NSF12FunctionKey) { + event.key = KeyboardEvent::Key::FUNCTION; + event.num = chr - NSF1FunctionKey + 1; + } else { + event.key = KeyboardEvent::Key::CHARACTER; + event.chr = chr; + } + + return event; +} + +- (void)keyDown:(NSEvent *)nsEvent { + using Platform::KeyboardEvent; + + if([NSEvent modifierFlags] & ~(NSEventModifierFlagShift|NSEventModifierFlagCommand)) { + [super keyDown:nsEvent]; + return; + } + + KeyboardEvent event = [self convertKeyboardEvent:nsEvent]; + event.type = KeyboardEvent::Type::PRESS; + + if(receiver->onKeyboardEvent) { + receiver->onKeyboardEvent(event); + return; + } + + [super keyDown:nsEvent]; +} + +- (void)keyUp:(NSEvent *)nsEvent { + using Platform::KeyboardEvent; + + if([NSEvent modifierFlags] & ~(NSEventModifierFlagShift|NSEventModifierFlagCommand)) { + [super keyUp:nsEvent]; + return; + } + + KeyboardEvent event = [self convertKeyboardEvent:nsEvent]; + event.type = KeyboardEvent::Type::RELEASE; + + if(receiver->onKeyboardEvent) { + receiver->onKeyboardEvent(event); + return; + } + + [super keyUp:nsEvent]; +} + +- (void)magnifyGesture:(NSMagnificationGestureRecognizer *)gesture { + // The onSixDofEvent API doesn't allow us to specify the scaling's origin, so for expediency + // we fake out a scrollwheel MouseEvent with a suitably-scaled scrollDelta with a bit of + // absolute-to-relative positioning conversion tracked using magnificationGestureCurrentZ. + + if(gesture.state == NSGestureRecognizerStateBegan) { + magnificationGestureCurrentZ = 0.0; + } + + // Magic number to make gesture.magnification align roughly with what scrollDelta expects + constexpr double kScale = 10.0; + double z = ((double)gesture.magnification * kScale); + double zdelta = z - magnificationGestureCurrentZ; + magnificationGestureCurrentZ = z; + + using Platform::MouseEvent; + MouseEvent event = {}; + event.type = MouseEvent::Type::SCROLL_VERT; + NSPoint nsPoint = [gesture locationInView:self]; + event.x = nsPoint.x; + event.y = nsPoint.y; + event.scrollDelta = zdelta; + if(receiver->onMouseEvent) { + receiver->onMouseEvent(event); + } +} + +- (void)rotateGesture:(NSRotationGestureRecognizer *)gesture { + if(gesture.state == NSGestureRecognizerStateBegan) { + rotationGestureCurrent = 0.0; + } + double rotation = gesture.rotation; + double rotationDelta = rotation - rotationGestureCurrent; + rotationGestureCurrent = rotation; + + using Platform::SixDofEvent; + SixDofEvent event = {}; + event.type = SixDofEvent::Type::MOTION; + event.rotationZ = rotationDelta; + if(receiver->onSixDofEvent) { + receiver->onSixDofEvent(event); + } +} + +@synthesize editing; + +- (void)startEditing:(NSString *)text at:(NSPoint)origin withHeight:(double)fontHeight + minWidth:(double)minWidth usingMonospace:(BOOL)isMonospace { + if(!editing) { + [self addSubview:editor]; + editing = YES; + } + + if(isMonospace) { + editor.font = [NSFont fontWithName:@"Monaco" size:fontHeight]; + } else { + editor.font = [NSFont controlContentFontOfSize:fontHeight]; + } + + origin.x -= 3; /* left padding; no way to get it from NSTextField */ + origin.y -= [editor intrinsicContentSize].height; + origin.y += [editor baselineOffsetFromBottom]; + + [editor setFrameOrigin:origin]; + [editor setStringValue:text]; + [editor sizeToFit]; + + NSSize frameSize = [editor frame].size; + frameSize.width = std::max(frameSize.width, minWidth); + [editor setFrameSize:frameSize]; + + [[self window] makeFirstResponder:editor]; + [[self window] makeKeyWindow]; +} + +- (void)stopEditing { + if(editing) { + [editor removeFromSuperview]; + [[self window] makeFirstResponder:self]; + editing = NO; + } +} + +- (void)didEdit:(id)sender { + if(receiver->onEditingDone) { + receiver->onEditingDone([[editor stringValue] UTF8String]); + } +} + +- (void)cancelOperation:(id)sender { + using Platform::KeyboardEvent; + + if(receiver->onKeyboardEvent) { + KeyboardEvent event = {}; + event.key = KeyboardEvent::Key::CHARACTER; + event.chr = '\e'; + event.type = KeyboardEvent::Type::PRESS; + receiver->onKeyboardEvent(event); + event.type = KeyboardEvent::Type::RELEASE; + receiver->onKeyboardEvent(event); + } +} + +@synthesize scrollerMin; +@synthesize scrollerSize; +@synthesize pageSize; + +- (void)didScroll:(NSScroller *)sender { + double pos; + switch(sender.hitPart) { + case NSScrollerKnob: + case NSScrollerKnobSlot: + pos = receiver->GetScrollbarPosition(); + break; + case NSScrollerDecrementPage: + pos = receiver->GetScrollbarPosition() - pageSize; + break; + case NSScrollerIncrementPage: + pos = receiver->GetScrollbarPosition() + pageSize; + break; + default: + return; + } + + if(receiver->onScrollbarAdjusted) { + receiver->onScrollbarAdjusted(pos); + } +} +@end + +@interface SSWindowDelegate : NSObject +@property Platform::Window *receiver; + +- (BOOL)windowShouldClose:(id)sender; + +@property(readonly, getter=isFullScreen) BOOL fullScreen; +- (void)windowDidEnterFullScreen:(NSNotification *)notification; +- (void)windowDidExitFullScreen:(NSNotification *)notification; +@end + +@implementation SSWindowDelegate +@synthesize receiver; + +- (BOOL)windowShouldClose:(id)sender { + if(receiver->onClose) { + receiver->onClose(); + } + return NO; +} + +@synthesize fullScreen; + +- (void)windowDidEnterFullScreen:(NSNotification *)notification { + fullScreen = true; + if(receiver->onFullScreen) { + receiver->onFullScreen(fullScreen); + } +} + +- (void)windowDidExitFullScreen:(NSNotification *)notification { + fullScreen = false; + if(receiver->onFullScreen) { + receiver->onFullScreen(fullScreen); + } +} +@end + +namespace SolveSpace { +namespace Platform { + +//----------------------------------------------------------------------------- +// Windows +//----------------------------------------------------------------------------- + +class WindowImplCocoa final : public Window { +public: + NSWindow *nsWindow; + SSWindowDelegate *ssWindowDelegate; + SSView *ssView; + NSScroller *nsScroller; + NSView *nsContainer; + + NSArray *nsConstraintsWithScrollbar; + NSArray *nsConstraintsWithoutScrollbar; + + double minWidth = 100.0; + double minHeight = 100.0; + + NSString *nsToolTip; + + WindowImplCocoa(Window::Kind kind, std::shared_ptr parentWindow) { + ssView = [[SSView alloc] initWithKind:kind]; + ssView.translatesAutoresizingMaskIntoConstraints = NO; + ssView.receiver = this; + + nsScroller = [[NSScroller alloc] initWithFrame:NSMakeRect(0, 0, 0, 100)]; + nsScroller.translatesAutoresizingMaskIntoConstraints = NO; + nsScroller.enabled = YES; + nsScroller.scrollerStyle = NSScrollerStyleOverlay; + nsScroller.knobStyle = NSScrollerKnobStyleLight; + nsScroller.action = @selector(didScroll:); + nsScroller.target = ssView; + nsScroller.continuous = YES; + + nsContainer = [[NSView alloc] init]; + [nsContainer addSubview:ssView]; + [nsContainer addSubview:nsScroller]; + + NSDictionary *views = NSDictionaryOfVariableBindings(ssView, nsScroller); + nsConstraintsWithoutScrollbar = [NSLayoutConstraint + constraintsWithVisualFormat:@"H:|[ssView]|" + options:0 metrics:nil views:views]; + [nsContainer addConstraints:nsConstraintsWithoutScrollbar]; + nsConstraintsWithScrollbar = [NSLayoutConstraint + constraintsWithVisualFormat:@"H:|[ssView]-0-[nsScroller(11)]|" + options:0 metrics:nil views:views]; + [nsContainer addConstraints:[NSLayoutConstraint + constraintsWithVisualFormat:@"V:|[ssView]|" + options:0 metrics:nil views:views]]; + [nsContainer addConstraints:[NSLayoutConstraint + constraintsWithVisualFormat:@"V:|[nsScroller]|" + options:0 metrics:nil views:views]]; + + switch(kind) { + case Window::Kind::TOPLEVEL: + nsWindow = [[NSWindow alloc] init]; + nsWindow.styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable | + NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; + nsWindow.collectionBehavior = NSWindowCollectionBehaviorFullScreenPrimary; + ssView.acceptsFirstResponder = YES; + break; + + case Window::Kind::TOOL: + NSPanel *nsPanel = [[NSPanel alloc] init]; + nsPanel.styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable | + NSWindowStyleMaskClosable | NSWindowStyleMaskUtilityWindow; + [nsPanel standardWindowButton:NSWindowMiniaturizeButton].hidden = YES; + [nsPanel standardWindowButton:NSWindowZoomButton].hidden = YES; + nsPanel.floatingPanel = YES; + nsPanel.becomesKeyOnlyIfNeeded = YES; + nsWindow = nsPanel; + break; + } + + ssWindowDelegate = [[SSWindowDelegate alloc] init]; + ssWindowDelegate.receiver = this; + nsWindow.delegate = ssWindowDelegate; + + nsWindow.backgroundColor = [NSColor blackColor]; + nsWindow.contentView = nsContainer; + } + + double GetPixelDensity() override { + NSDictionary *description = nsWindow.screen.deviceDescription; + NSSize displayPixelSize = [[description objectForKey:NSDeviceSize] sizeValue]; + CGSize displayPhysicalSize = CGDisplayScreenSize( + [[description objectForKey:@"NSScreenNumber"] unsignedIntValue]); + return (displayPixelSize.width / displayPhysicalSize.width) * 25.4f; + } + + double GetDevicePixelRatio() override { + NSSize unitSize = { 1.0f, 0.0f }; + unitSize = [ssView convertSizeToBacking:unitSize]; + return unitSize.width; + } + + bool IsVisible() override { + return ![nsWindow isVisible]; + } + + void SetVisible(bool visible) override { + if(visible) { + [nsWindow orderFront:nil]; + } else { + [nsWindow close]; + } + } + + void Focus() override { + [nsWindow makeKeyAndOrderFront:nil]; + } + + bool IsFullScreen() override { + return ssWindowDelegate.fullScreen; + } + + void SetFullScreen(bool fullScreen) override { + if(fullScreen != IsFullScreen()) { + [nsWindow toggleFullScreen:nil]; + } + } + + void SetTitle(const std::string &title) override { + nsWindow.representedFilename = @""; + nsWindow.title = Wrap(title); + } + + bool SetTitleForFilename(const Path &filename) override { + [nsWindow setTitleWithRepresentedFilename:Wrap(filename.raw)]; + return true; + } + + void SetMenuBar(MenuBarRef newMenuBar) override { + // Doesn't do anything, since we have an unique global menu bar. + } + + void GetContentSize(double *width, double *height) override { + NSSize nsSize = ssView.frame.size; + *width = nsSize.width; + *height = nsSize.height; + } + + void SetMinContentSize(double width, double height) override { + NSSize nsMinSize; + nsMinSize.width = width; + nsMinSize.height = height; + [nsWindow setContentMinSize:nsMinSize]; + [nsWindow setContentSize:nsMinSize]; + } + + void FreezePosition(SettingsRef _settings, const std::string &key) override { + [nsWindow saveFrameUsingName:Wrap(key)]; + } + + void ThawPosition(SettingsRef _settings, const std::string &key) override { + [nsWindow setFrameUsingName:Wrap(key)]; + } + + void SetCursor(Cursor cursor) override { + NSCursor *nsNewCursor; + switch(cursor) { + case Cursor::POINTER: nsNewCursor = [NSCursor arrowCursor]; break; + case Cursor::HAND: nsNewCursor = [NSCursor pointingHandCursor]; break; + } + + if([NSCursor currentCursor] != nsNewCursor) { + [nsNewCursor set]; + } + } + + void SetTooltip(const std::string &newText, double x, double y, double w, double h) override { + NSString *nsNewText = Wrap(newText); + if(![nsToolTip isEqualToString:nsNewText]) { + nsToolTip = nsNewText; + + NSToolTipManager *nsToolTipManager = [NSToolTipManager sharedToolTipManager]; + if(newText.empty()) { + if ([nsToolTipManager respondsToSelector:@selector(abortToolTip)]) { + [nsToolTipManager abortToolTip]; + } else { + [nsToolTipManager orderOutToolTip]; + } + } else { + [nsToolTipManager _displayTemporaryToolTipForView:ssView withString:Wrap(newText)]; + } + } + } + + bool IsEditorVisible() override { + return [ssView isEditing]; + } + + void ShowEditor(double x, double y, double fontHeight, double minWidth, + bool isMonospace, const std::string &text) override { + [ssView startEditing:Wrap(text) at:(NSPoint){(CGFloat)x, (CGFloat)y} + withHeight:fontHeight minWidth:minWidth usingMonospace:isMonospace]; + } + + void HideEditor() override { + [ssView stopEditing]; + } + + void SetScrollbarVisible(bool visible) override { + if(visible) { + [nsContainer removeConstraints:nsConstraintsWithoutScrollbar]; + [nsContainer addConstraints:nsConstraintsWithScrollbar]; + } else { + [nsContainer removeConstraints:nsConstraintsWithScrollbar]; + [nsContainer addConstraints:nsConstraintsWithoutScrollbar]; + } + } + + void ConfigureScrollbar(double min, double max, double pageSize) override { + ssView.scrollerMin = min; + ssView.scrollerSize = max + 1 - min; + ssView.pageSize = pageSize; + nsScroller.knobProportion = pageSize / ssView.scrollerSize; + nsScroller.hidden = pageSize >= ssView.scrollerSize; + } + + double GetScrollbarPosition() override { + // Platform::Window scrollbar positions are in the range [min, max+1 - pageSize] inclusive, + // and Cocoa scrollbars are from 0.0 to 1.0 inclusive, so we have to apply some scaling and + // transforming. (scrollerSize is max+1-min, see ConfigureScrollbar above) + return ssView.scrollerMin + + nsScroller.doubleValue * (ssView.scrollerSize - ssView.pageSize); + } + + void SetScrollbarPosition(double pos) override { + nsScroller.doubleValue = (pos - ssView.scrollerMin) / ( ssView.scrollerSize - ssView.pageSize); + } + + void Invalidate() override { + ssView.needsDisplay = YES; + } +}; + +WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { + return std::make_shared(kind, + std::static_pointer_cast(parentWindow)); +} + +//----------------------------------------------------------------------------- +// 3DConnexion support +//----------------------------------------------------------------------------- + +// Normally we would just link to the 3DconnexionClient framework. +// +// We don't want to (are not allowed to) distribute the official framework, so we're trying +// to use the one installed on the users' computer. There are some different versions of +// the framework, the official one and re-implementations using an open source driver for +// older devices (spacenav-plus). So weak-linking isn't an option, either. The only remaining +// way is using CFBundle to dynamically load the library at runtime, and also detect its +// availability. +// +// We're also defining everything needed from the 3DconnexionClientAPI, so we're not depending +// on the API headers. + +#pragma pack(push,2) + +enum { + kConnexionClientModeTakeOver = 1, + kConnexionClientModePlugin = 2 +}; + +#define kConnexionMsgDeviceState '3dSR' +#define kConnexionMaskButtons 0x00FF +#define kConnexionMaskAxis 0x3F00 + +typedef struct { + uint16_t version; + uint16_t client; + uint16_t command; + int16_t param; + int32_t value; + UInt64 time; + uint8_t report[8]; + uint16_t buttons8; + int16_t axis[6]; + uint16_t address; + uint32_t buttons; +} ConnexionDeviceState, *ConnexionDeviceStatePtr; + +#pragma pack(pop) + +typedef void (*ConnexionAddedHandlerProc)(io_connect_t); +typedef void (*ConnexionRemovedHandlerProc)(io_connect_t); +typedef void (*ConnexionMessageHandlerProc)(io_connect_t, natural_t, void *); + +typedef OSErr (*InstallConnexionHandlersProc)(ConnexionMessageHandlerProc, ConnexionAddedHandlerProc, ConnexionRemovedHandlerProc); +typedef void (*CleanupConnexionHandlersProc)(void); +typedef UInt16 (*RegisterConnexionClientProc)(UInt32, UInt8 *, UInt16, UInt32); +typedef void (*UnregisterConnexionClientProc)(UInt16); + +static CFBundleRef spaceBundle = nil; +static InstallConnexionHandlersProc installConnexionHandlers = NULL; +static CleanupConnexionHandlersProc cleanupConnexionHandlers = NULL; +static RegisterConnexionClientProc registerConnexionClient = NULL; +static UnregisterConnexionClientProc unregisterConnexionClient = NULL; +static UInt32 connexionSignature = 'SoSp'; +static UInt8 *connexionName = (UInt8 *)"\x10SolveSpace"; +static UInt16 connexionClient = 0; + +static std::vector> connexionWindows; +static bool connexionShiftIsDown = false; +static bool connexionCommandIsDown = false; + +static void ConnexionAdded(io_connect_t con) {} +static void ConnexionRemoved(io_connect_t con) {} +static void ConnexionMessage(io_connect_t con, natural_t type, void *arg) { + if (type != kConnexionMsgDeviceState) { + return; + } + + ConnexionDeviceState *device = (ConnexionDeviceState *)arg; + + dispatch_async(dispatch_get_main_queue(), ^(void){ + SixDofEvent event = {}; + event.type = SixDofEvent::Type::MOTION; + event.translationX = (double)device->axis[0] * -0.25; + event.translationY = (double)device->axis[1] * -0.25; + event.translationZ = (double)device->axis[2] * 0.25; + event.rotationX = (double)device->axis[3] * -0.0005; + event.rotationY = (double)device->axis[4] * -0.0005; + event.rotationZ = (double)device->axis[5] * -0.0005; + event.shiftDown = connexionShiftIsDown; + event.controlDown = connexionCommandIsDown; + + for(auto window : connexionWindows) { + if(auto windowStrong = window.lock()) { + if(windowStrong->onSixDofEvent) { + windowStrong->onSixDofEvent(event); + } + } + } + }); +} + +void Open3DConnexion() { + NSString *bundlePath = @"/Library/Frameworks/3DconnexionClient.framework"; + NSURL *bundleURL = [NSURL fileURLWithPath:bundlePath]; + spaceBundle = CFBundleCreate(kCFAllocatorDefault, (__bridge CFURLRef)bundleURL); + + // Don't continue if no driver is installed on this machine + if(spaceBundle == nil) { + return; + } + + installConnexionHandlers = (InstallConnexionHandlersProc) + CFBundleGetFunctionPointerForName(spaceBundle, + CFSTR("InstallConnexionHandlers")); + + cleanupConnexionHandlers = (CleanupConnexionHandlersProc) + CFBundleGetFunctionPointerForName(spaceBundle, + CFSTR("CleanupConnexionHandlers")); + + registerConnexionClient = (RegisterConnexionClientProc) + CFBundleGetFunctionPointerForName(spaceBundle, + CFSTR("RegisterConnexionClient")); + + unregisterConnexionClient = (UnregisterConnexionClientProc) + CFBundleGetFunctionPointerForName(spaceBundle, + CFSTR("UnregisterConnexionClient")); + + // Only continue if all required symbols have been loaded + if((installConnexionHandlers == NULL) || (cleanupConnexionHandlers == NULL) + || (registerConnexionClient == NULL) || (unregisterConnexionClient == NULL)) { + CFRelease(spaceBundle); + spaceBundle = nil; + return; + } + + installConnexionHandlers(&ConnexionMessage, &ConnexionAdded, &ConnexionRemoved); + connexionClient = registerConnexionClient(connexionSignature, connexionName, + kConnexionClientModeTakeOver, kConnexionMaskButtons | kConnexionMaskAxis); + + [NSEvent addLocalMonitorForEventsMatchingMask:(NSEventMaskKeyDown | NSEventMaskFlagsChanged) + handler:^(NSEvent *event) { + connexionShiftIsDown = (event.modifierFlags & NSEventModifierFlagShift); + connexionCommandIsDown = (event.modifierFlags & NSEventModifierFlagCommand); + return event; + }]; + + [NSEvent addLocalMonitorForEventsMatchingMask:(NSEventMaskKeyUp | NSEventMaskFlagsChanged) + handler:^(NSEvent *event) { + connexionShiftIsDown = (event.modifierFlags & NSEventModifierFlagShift); + connexionCommandIsDown = (event.modifierFlags & NSEventModifierFlagCommand); + return event; + }]; +} + +void Close3DConnexion() { + if(spaceBundle == nil) { + return; + } + + unregisterConnexionClient(connexionClient); + cleanupConnexionHandlers(); + + CFRelease(spaceBundle); +} + +void Request3DConnexionEventsForWindow(WindowRef window) { + connexionWindows.push_back(window); +} + +//----------------------------------------------------------------------------- +// Message dialogs +//----------------------------------------------------------------------------- + +class MessageDialogImplCocoa final : public MessageDialog { +public: + NSAlert *nsAlert = [[NSAlert alloc] init]; + NSWindow *nsWindow; + + std::vector responses; + + void SetType(Type type) override { + switch(type) { + case Type::INFORMATION: + case Type::QUESTION: + nsAlert.alertStyle = NSAlertStyleInformational; + break; + + case Type::WARNING: + case Type::ERROR: + nsAlert.alertStyle = NSAlertStyleWarning; + break; + } + } + + void SetTitle(std::string title) override { + [nsAlert.window setTitle:Wrap(title)]; + } + + void SetMessage(std::string message) override { + nsAlert.messageText = Wrap(message); + } + + void SetDescription(std::string description) override { + nsAlert.informativeText = Wrap(description); + } + + void AddButton(std::string label, Response response, bool isDefault) override { + NSButton *nsButton = [nsAlert addButtonWithTitle:Wrap(PrepareMnemonics(label))]; + if(!isDefault && [nsButton.keyEquivalent isEqualToString:@"\n"]) { + nsButton.keyEquivalent = @""; + } else if(response == Response::CANCEL) { + nsButton.keyEquivalent = @"\e"; + } + responses.push_back(response); + } + + Response RunModal() override { + // FIXME(platform/gui): figure out a way to run the alert as a sheet + NSModalResponse nsResponse = [nsAlert runModal]; + ssassert(nsResponse >= NSAlertFirstButtonReturn && + nsResponse <= NSAlertFirstButtonReturn + (long)responses.size(), + "Unexpected response"); + return responses[nsResponse - NSAlertFirstButtonReturn]; + } +}; + +MessageDialogRef CreateMessageDialog(WindowRef parentWindow) { + std::shared_ptr dialog = std::make_shared(); + dialog->nsWindow = std::static_pointer_cast(parentWindow)->nsWindow; + return dialog; +} + +//----------------------------------------------------------------------------- +// File dialogs +//----------------------------------------------------------------------------- + +} +} + +@interface SSSaveFormatAccessory : NSViewController +@property NSSavePanel *panel; +@property NSMutableArray *filters; + +@property(nonatomic) NSInteger index; +@property(nonatomic) IBOutlet NSTextField *textField; +@property(nonatomic) IBOutlet NSPopUpButton *button; +@end + +@implementation SSSaveFormatAccessory +@synthesize panel, filters, button; + +- (void)setIndex:(NSInteger)newIndex { + self->_index = newIndex; + NSMutableArray *filter = [filters objectAtIndex:newIndex]; + NSString *extension = [filter objectAtIndex:0]; + if(![extension isEqual:@"*"]) { + NSString *filename = panel.nameFieldStringValue; + NSString *basename = [[filename componentsSeparatedByString:@"."] objectAtIndex:0]; + panel.nameFieldStringValue = [basename stringByAppendingPathExtension:extension]; + } + [panel setAllowedFileTypes:filter]; +} +@end + +namespace SolveSpace { +namespace Platform { + +class FileDialogImplCocoa : public FileDialog { +public: + NSSavePanel *nsPanel = nil; + + void SetTitle(std::string title) override { + nsPanel.title = Wrap(title); + } + + void SetCurrentName(std::string name) override { + nsPanel.nameFieldStringValue = Wrap(name); + } + + Platform::Path GetFilename() override { + return Platform::Path::From(nsPanel.URL.fileSystemRepresentation); + } + + void SetFilename(Platform::Path path) override { + nsPanel.directoryURL = + [NSURL fileURLWithPath:Wrap(path.Parent().raw) isDirectory:YES]; + nsPanel.nameFieldStringValue = Wrap(path.FileStem()); + } + + void SuggestFilename(Platform::Path path) override { + SetFilename(path.WithExtension("")); + } + + void FreezeChoices(SettingsRef settings, const std::string &key) override { + if (nsPanel.directoryURL != nil) { + settings->FreezeString("Dialog_" + key + "_Folder", + [nsPanel.directoryURL.absoluteString UTF8String]); + } + } + + void ThawChoices(SettingsRef settings, const std::string &key) override { + nsPanel.directoryURL = + [NSURL URLWithString:Wrap(settings->ThawString("Dialog_" + key + "_Folder", ""))]; + } + + bool RunModal() override { + if([nsPanel runModal] == NSModalResponseOK) { + return true; + } else { + return false; + } + } +}; + +class OpenFileDialogImplCocoa final : public FileDialogImplCocoa { +public: + NSMutableArray *nsFilter = [[NSMutableArray alloc] init]; + + OpenFileDialogImplCocoa() { + SetTitle(C_("title", "Open File")); + } + + void AddFilter(std::string name, std::vector extensions) override { + for(auto extension : extensions) { + [nsFilter addObject:Wrap(extension)]; + } + [nsPanel setAllowedFileTypes:nsFilter]; + } +}; + +class SaveFileDialogImplCocoa final : public FileDialogImplCocoa { +public: + NSMutableArray *nsFilters = [[NSMutableArray alloc] init]; + SSSaveFormatAccessory *ssAccessory = nil; + + SaveFileDialogImplCocoa() { + SetTitle(C_("title", "Save File")); + } + + void AddFilter(std::string name, std::vector extensions) override { + NSMutableArray *nsFilter = [[NSMutableArray alloc] init]; + for(auto extension : extensions) { + [nsFilter addObject:Wrap(extension)]; + } + if(nsFilters.count == 0) { + [nsPanel setAllowedFileTypes:nsFilter]; + } + [nsFilters addObject:nsFilter]; + + std::string desc; + for(auto extension : extensions) { + if(!desc.empty()) desc += ", "; + desc += extension; + } + std::string title = name + " (" + desc + ")"; + if(nsFilters.count == 1) { + [ssAccessory.button removeAllItems]; + } + [ssAccessory.button addItemWithTitle:Wrap(title)]; + [ssAccessory.button synchronizeTitleAndSelectedItem]; + } + + void FreezeChoices(SettingsRef settings, const std::string &key) override { + FileDialogImplCocoa::FreezeChoices(settings, key); + settings->FreezeInt("Dialog_" + key + "_Filter", ssAccessory.index); + } + + void ThawChoices(SettingsRef settings, const std::string &key) override { + FileDialogImplCocoa::ThawChoices(settings, key); + ssAccessory.index = settings->ThawInt("Dialog_" + key + "_Filter", 0); + } + + bool RunModal() override { + if(nsFilters.count == 1) { + nsPanel.accessoryView = nil; + } + + if(nsPanel.nameFieldStringValue.length == 0) { + nsPanel.nameFieldStringValue = Wrap(_("untitled")); + } + + return FileDialogImplCocoa::RunModal(); + } +}; + +FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) { + NSOpenPanel *nsPanel = [NSOpenPanel openPanel]; + nsPanel.canSelectHiddenExtension = YES; + + std::shared_ptr dialog = std::make_shared(); + dialog->nsPanel = nsPanel; + + return dialog; +} + +FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) { + NSSavePanel *nsPanel = [NSSavePanel savePanel]; + nsPanel.canSelectHiddenExtension = YES; + + SSSaveFormatAccessory *ssAccessory = + [[SSSaveFormatAccessory alloc] initWithNibName:@"SaveFormatAccessory" bundle:nil]; + ssAccessory.panel = nsPanel; + nsPanel.accessoryView = [ssAccessory view]; + + std::shared_ptr dialog = std::make_shared(); + dialog->nsPanel = nsPanel; + dialog->ssAccessory = ssAccessory; + ssAccessory.filters = dialog->nsFilters; + + return dialog; +} + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +std::vector GetFontFiles() { + std::vector fonts; + + NSArray *fontNames = [[NSFontManager sharedFontManager] availableFonts]; + for(NSString *fontName in fontNames) { + CTFontDescriptorRef fontRef = + CTFontDescriptorCreateWithNameAndSize ((__bridge CFStringRef)fontName, 10.0); + CFURLRef url = (CFURLRef)CTFontDescriptorCopyAttribute(fontRef, kCTFontURLAttribute); + NSString *fontPath = [NSString stringWithString:[(NSURL *)CFBridgingRelease(url) path]]; + fonts.push_back( + Platform::Path::From([[NSFileManager defaultManager] + fileSystemRepresentationWithPath:fontPath])); + } + + return fonts; +} + +void OpenInBrowser(const std::string &url) { + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:Wrap(url)]]; +} + +} +} + +@interface SSApplicationDelegate : NSObject +- (IBAction)preferences:(id)sender; +- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename; +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender; + +@property BOOL exiting; + +@end + +@implementation SSApplicationDelegate + +@synthesize exiting; + +- (id)init { + if (self = [super init]) { + self.exiting = false; + } + return self; +} + +- (IBAction)preferences:(id)sender { + if (!SS.GW.showTextWindow) { + SolveSpace::SS.GW.MenuView(SolveSpace::Command::SHOW_TEXT_WND); + } + SolveSpace::SS.TW.GoToScreen(SolveSpace::TextWindow::Screen::CONFIGURATION); + SolveSpace::SS.ScheduleShowTW(); +} + +- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename { + SolveSpace::Platform::Path path = SolveSpace::Platform::Path::From([filename UTF8String]); + return SolveSpace::SS.Load(path.Expand(/*fromCurrentDirectory=*/true)); +} + +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { + if(!SS.unsaved) { + return NSTerminateNow; + } else { + [self performSelectorOnMainThread:@selector(applicationTerminatePrompt) withObject:nil + waitUntilDone:NO modes:@[NSDefaultRunLoopMode, NSModalPanelRunLoopMode]]; + return NSTerminateLater; + } +} + +- (void)applicationWillTerminate:(NSNotification *)notification { + if(!exiting) { + // Prevent the Platform::ExitGui() call from SolveSpaceUI::Exit() + // triggering another terminate + exiting = true; + // Now let SS save settings etc + SS.Exit(); + } +} + +- (void)applicationTerminatePrompt { + [NSApp replyToApplicationShouldTerminate:SS.OkayToStartNewFile()]; +} +@end + +namespace SolveSpace { +namespace Platform { + +static SSApplicationDelegate *ssDelegate; + +std::vector InitGui(int argc, char **argv) { + std::vector args = InitCli(argc, argv); + if(args.size() >= 2 && args[1].find("-psn_") == 0) { + // For unknown reasons, Finder passes a Carbon PSN (Process Serial Number) argument + // when a freshly downloaded application is run for the first time. Remove it so + // that it isn't interpreted as a filename. + args.erase(args.begin() + 1); + } + + ssDelegate = [[SSApplicationDelegate alloc] init]; + NSApplication.sharedApplication.delegate = ssDelegate; + + // Setting this prevents "Show Tab Bar" and "Show All Tabs" items from being + // automagically added to the View menu + NSWindow.allowsAutomaticWindowTabbing = NO; + + // And this prevents the duplicate "Enter Full Screen" menu item, see + // https://stackoverflow.com/questions/52154977/how-to-get-rid-of-enter-full-screen-menu-item + [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"NSFullScreenMenuItemEverywhere"]; + + [NSBundle.mainBundle loadNibNamed:@"MainMenu" owner:nil topLevelObjects:nil]; + + NSArray *languages = NSLocale.preferredLanguages; + for(NSString *language in languages) { + if(SolveSpace::SetLocale([language UTF8String])) break; + } + if(languages.count == 0) { + SolveSpace::SetLocale("en_US"); + } + + return args; +} + +void RunGui() { + [NSApp run]; +} + +void ExitGui() { + if(!ssDelegate.exiting) { + ssDelegate.exiting = true; + [NSApp terminate:nil]; + } +} + +void ClearGui() {} + +} +} diff --git a/src/platform/guinone.cpp b/src/platform/guinone.cpp new file mode 100644 index 0000000..4641b98 --- /dev/null +++ b/src/platform/guinone.cpp @@ -0,0 +1,145 @@ +//----------------------------------------------------------------------------- +// Our platform support functions for the headless (no OpenGL) test runner. +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- +#include "solvespace.h" +#include + +namespace SolveSpace { + +//----------------------------------------------------------------------------- +// Rendering +//----------------------------------------------------------------------------- + +std::shared_ptr CreateRenderer() { + return std::make_shared(); +} + +namespace Platform { + +//----------------------------------------------------------------------------- +// Fatal errors +//----------------------------------------------------------------------------- + +void FatalError(const std::string &message) { + fprintf(stderr, "%s", message.c_str()); + abort(); +} + +//----------------------------------------------------------------------------- +// Settings +//----------------------------------------------------------------------------- + +class SettingsImplDummy final : public Settings { +public: + void FreezeInt(const std::string &key, uint32_t value) override { + } + + uint32_t ThawInt(const std::string &key, uint32_t defaultValue = 0) override { + return defaultValue; + } + + void FreezeFloat(const std::string &key, double value) override { + } + + double ThawFloat(const std::string &key, double defaultValue = 0.0) override { + return defaultValue; + } + + void FreezeString(const std::string &key, const std::string &value) override { + } + + std::string ThawString(const std::string &key, const std::string &defaultValue = "") override { + return defaultValue; + } +}; + +SettingsRef GetSettings() { + static std::shared_ptr settings = + std::make_shared(); + return settings; +} + +//----------------------------------------------------------------------------- +// Timers +//----------------------------------------------------------------------------- + +class TimerImplDummy final : public Timer { +public: + void RunAfter(unsigned milliseconds) override {} +}; + +TimerRef CreateTimer() { + return std::make_shared(); +} + +//----------------------------------------------------------------------------- +// Menus +//----------------------------------------------------------------------------- + +MenuRef CreateMenu() { + return std::shared_ptr(); +} + +MenuBarRef GetOrCreateMainMenu(bool *unique) { + *unique = false; + return std::shared_ptr(); +} + +//----------------------------------------------------------------------------- +// Windows +//----------------------------------------------------------------------------- + +WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { + return std::shared_ptr(); +} + +void Request3DConnexionEventsForWindow(WindowRef window) {} + +//----------------------------------------------------------------------------- +// Message dialogs +//----------------------------------------------------------------------------- + +MessageDialogRef CreateMessageDialog(WindowRef parentWindow) { + return std::shared_ptr(); +} + +//----------------------------------------------------------------------------- +// File dialogs +//----------------------------------------------------------------------------- + +FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) { + return std::shared_ptr(); +} + +FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) { + return std::shared_ptr(); +} + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +std::vector fontFiles; +std::vector GetFontFiles() { + return fontFiles; +} + +void OpenInBrowser(const std::string &url) {} + +std::vector InitGui(int argc, char **argv) { + return {}; +} + +void RunGui() {} + +void ExitGui() { + exit(0); +} + +void ClearGui() {} + +} + +} diff --git a/src/platform/guiqt.cpp b/src/platform/guiqt.cpp new file mode 100644 index 0000000..dcee6ea --- /dev/null +++ b/src/platform/guiqt.cpp @@ -0,0 +1,1076 @@ +//----------------------------------------------------------------------------- +// The Qt-based implementation of platform-dependent GUI functionality. +// +// Copyright 2024 Karl Robillard +// Copyright 2023 Raed Marwan +// SPDX-License-Identifier: GPL-3.0-or-later +//----------------------------------------------------------------------------- + +#include +#include "solvespace.h" +#include "platform.h" +#include "guiqt.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef WIN32 +# include +#else +# include +#endif + +namespace SolveSpace { +namespace Platform { + +//----------------------------------------------------------------------------- +// Fatal errors +//----------------------------------------------------------------------------- + +void FatalError(const std::string& message) +{ + QMessageBox::critical(NULL, QString("Fatal Error"), + QString::fromStdString(message), + QMessageBox::StandardButton::Ok); + abort(); +} + +//----------------------------------------------------------------------------- +// Message dialogs +//----------------------------------------------------------------------------- + +class MessageDialogImplQt final : public MessageDialog { +public: + QMessageBox messageBoxQ; + + MessageDialogImplQt(QWidget* parent) : messageBoxQ(parent) { + } + +#undef ERROR + void SetType(Type type) override { + switch (type) { + case Type::INFORMATION: + messageBoxQ.setIcon(QMessageBox::Information); + break; + case Type::QUESTION: + messageBoxQ.setIcon(QMessageBox::Question); + break; + case Type::WARNING: + messageBoxQ.setIcon(QMessageBox::Warning); + break; + case Type::ERROR: + messageBoxQ.setIcon(QMessageBox::Critical); + break; + } + } + + void SetTitle(std::string title) override { + messageBoxQ.setWindowTitle(QString::fromStdString(title)); + } + + void SetMessage(std::string message) override { + messageBoxQ.setText(QString::fromStdString(message)); + } + + void SetDescription(std::string description) override { + messageBoxQ.setInformativeText(QString::fromStdString(description)); + } + + void AddButton(std::string label, Response response, bool isDefault) override { + QMessageBox::StandardButton std; + switch (response) { + case Response::NONE: ssassert(false, "Invalid response"); + case Response::OK: + std = QMessageBox::StandardButton::Ok; + break; + case Response::YES: + std = QMessageBox::StandardButton::Yes; + break; + case Response::NO: + std = QMessageBox::StandardButton::No; + break; + case Response::CANCEL: + std = QMessageBox::StandardButton::Cancel; + break; + } + QPushButton* button = messageBoxQ.addButton(std); + button->setText(QString::fromStdString(label)); + if (isDefault) + messageBoxQ.setDefaultButton(button); + } + + Response RunModal() { + switch (messageBoxQ.exec()) + { + case QMessageBox::StandardButton::Cancel: + return Response::CANCEL; + case QMessageBox::StandardButton::No: + return Response::NO; + case QMessageBox::StandardButton::Yes: + return Response::YES; + case QMessageBox::StandardButton::Ok: + return Response::OK; + default: // NONE + return Response::NONE; + } + } +}; + +//----------------------------------------------------------------------------- +// File dialogs +//----------------------------------------------------------------------------- + +class FileDialogImplQt final : public FileDialog { +public: + QFileDialog fileDialogQ; + QStringList filters; + bool filtersMod; + + FileDialogImplQt(QWidget* parent, bool save) : filtersMod(false) { + fileDialogQ.setParent(parent); + fileDialogQ.setWindowFlags(Qt::Dialog); // Reset after setParent. + + // NOTE: The file extension is automatically set by the KDE native + // dialog but not with the Qt one. +#if 0 + fileDialogQ.setOption(QFileDialog::DontUseNativeDialog); +#endif + + if (save) { + fileDialogQ.setAcceptMode(QFileDialog::AcceptSave); + } + } + + void SetTitle(std::string title) { + fileDialogQ.setWindowTitle(QString::fromStdString(title)); + } + + void SetCurrentName(std::string name) { + fileDialogQ.selectFile(QString::fromStdString(name)); + } + + Platform::Path GetFilename() { + QStringList files = fileDialogQ.selectedFiles(); + return Path::From(QDir::toNativeSeparators(files[0]).toStdString()); + } + + void SetFilename(Platform::Path path) { + fileDialogQ.selectFile(QString::fromStdString(path.raw)); + } + + void SuggestFilename(Platform::Path path) { + fileDialogQ.selectFile(QString::fromStdString(path.FileStem())); + } + + void AddFilter(std::string name, std::vector extensions) override { + QString fline(name.c_str()); + int count = 0; + for (auto& ext : extensions) { + fline.append(count ? " *." : " (*."); + fline.append(ext.c_str()); + ++count; + } + fline.append(")"); + + //printf("AddFilter \"%s\"\n", fline.toLocal8Bit().data()); + filters.append(fline); + filtersMod = true; + } + + void updateFilters() { + if (filtersMod) { + filtersMod = false; + fileDialogQ.setNameFilters(filters); + } + } + + void FreezeChoices(SettingsRef settings, const std::string& key) + { + settings->FreezeString("Dialog_" + key + "_Folder", + fileDialogQ.directory().absolutePath().toStdString()); + settings->FreezeString("Dialog_" + key + "_Filter", + fileDialogQ.selectedNameFilter().toStdString()); + } + + void ThawChoices(SettingsRef settings, const std::string &key) + { + updateFilters(); + + std::string val; + val = settings->ThawString("Dialog_" + key + "_Folder"); + fileDialogQ.setDirectory(QDir(QString::fromStdString(val))); + val = settings->ThawString("Dialog_" + key + "_Filter"); + fileDialogQ.selectNameFilter(QString::fromStdString(val)); + } + + bool RunModal() override { + updateFilters(); + return fileDialogQ.exec(); + } +}; + +//----------------------------------------------------------------------------- +// Settings +//----------------------------------------------------------------------------- + +class SettingsImplQt final : public Settings { +public: + QSettings qset; + + SettingsImplQt() : qset("SolveSpace", "solvespace") { + } + + void FreezeInt(const std::string& key, uint32_t value) override { + qset.setValue(QString::fromStdString(key), value); + } + + uint32_t ThawInt(const std::string& key, uint32_t defaultValue = 0) override { + return qset.value(QString::fromStdString(key), defaultValue).toInt(); + } + + void FreezeFloat(const std::string& key, double value) override{ + qset.setValue(QString::fromStdString(key), value); + } + + double ThawFloat(const std::string& key, double defaultValue = 0.0) override { + return qset.value(QString::fromStdString(key), defaultValue).toDouble(); + } + + void FreezeString(const std::string& key, const std::string& value) override { + qset.setValue(QString::fromStdString(key), + QString::fromStdString(value)); + } + + std::string ThawString(const std::string& key, const std::string& defaultValue = "") override { + return qset.value(QString::fromStdString(key), + QString::fromStdString(defaultValue)).toString().toStdString(); + } +}; + +SettingsRef GetSettings() +{ + static std::shared_ptr settings; + if (!settings) { + settings = std::make_shared(); + } + return settings; +} + +//----------------------------------------------------------------------------- +// Timers +//----------------------------------------------------------------------------- + +class TimerImplQt final : public Timer, public QObject +{ +public: + TimerImplQt() {} + + void RunAfter(unsigned milliseconds) override { + QTimer::singleShot(milliseconds, this, &TimerImplQt::runOnTimeOut); + } + + void runOnTimeOut() { + if(onTimeout) { + onTimeout(); + } + } +}; + +TimerRef CreateTimer() { + std::shared_ptr timer = std::make_shared(); + return timer; +} + +//----------------------------------------------------------------------------- +// Menus +//----------------------------------------------------------------------------- + +class MenuItemImplQt final : public MenuItem, public QObject { +public: + QAction action; + + MenuItemImplQt() { + connect(&action, &QAction::triggered, this, &MenuItemImplQt::onTriggered); + } + + void SetAccelerator(KeyboardEvent accel) override { + int key = 0; + + if (accel.key == KeyboardEvent::Key::CHARACTER) { + if (accel.chr == '\t') { + key = Qt::Key_Tab; + } else if (accel.chr == '\x1b') { + key = Qt::Key_Escape; + } else if (accel.chr == '\x7f') { + key = Qt::Key_Delete; + } else { + key = std::toupper(accel.chr); + } + } else if (accel.key == KeyboardEvent::Key::FUNCTION) { + key = Qt::Key_F1 + accel.num - 1; + } + + if (key) { + if (accel.controlDown) { + key |= Qt::CTRL; + } + if (accel.shiftDown) { + key |= Qt::SHIFT; + } + action.setShortcut(key); + } + } + + /* + RADIO_MARK must be implemented using QActionGroup so we scan for + sequential actions with the "radio_mark" property to form these groups. + It would be safer if the Indicator type was an argument to + Platform::Menu::AddItem rather than a set by a separate method to guarantee + that the Indicator type can never change. + */ + void SetIndicator(Indicator type) override { + switch (type) + { + case Indicator::NONE: + action.setCheckable(false); + break; + case Indicator::CHECK_MARK: + action.setCheckable(true); + break; + case Indicator::RADIO_MARK: + action.setCheckable(true); + // Added to QActionGroup later by formActionGroups(). + action.setProperty("radio_mark", true); + break; + } + } + + void SetEnabled(bool enabled) override { + action.setEnabled(enabled); + } + + void SetActive(bool active) override { + action.setChecked(active); + } + +public slots: + void onTriggered(bool value) + { + if (onTrigger) + onTrigger(); + } +}; + +class MenuImplQt final : public Menu { +public: + QMenu* menuQ; + bool hasParent; + std::vector> menuItems; + std::vector> subMenus; + + MenuImplQt(bool hasParentParam = true) { + menuQ = new QMenu(); + hasParent = hasParentParam; + } + + MenuImplQt(QMenu* menuQParam) { + menuQ = menuQParam; + hasParent = true; + } + + std::shared_ptr AddItem(const std::string& label, std::function onTrigger, + bool /*mnemonics*/) override { + + std::shared_ptr menuItem = std::make_shared(); + menuItem->onTrigger = onTrigger; + menuItem->action.setText(QString::fromStdString(label)); + // I do not think anything is needed for mnemonics flag. + // the shortcut key sequence is set in the menuItem class and Qt acts + // accordingly with no further activation ... I think + menuItems.push_back(menuItem); + + // The QWidget::addAction method does not take ownership of the action. + menuQ->addAction(&menuItem->action); + return menuItem; + } + + std::shared_ptr AddSubMenu(const std::string& label) override { + std::shared_ptr subMenu = std::make_shared(menuQ->addMenu(QString::fromStdString(label))); + subMenus.push_back(subMenu); + return subMenu; + } + + void AddSeparator() override { + menuQ->addSeparator(); + } + + void PopUp() override { + if (true == this->hasParent) + return; + + menuQ->popup(menuQ->mapFromGlobal(QCursor::pos())); + menuQ->exec(); + } + + void Clear() override { + menuQ->clear(); + menuItems.clear(); + subMenus.clear(); + } +}; + +MenuRef CreateMenu() { + return std::make_shared(false); // false says -> menu has not Qtwidget as a parent +} + +class MenuBarImpQt final : public MenuBar { +public: + QMenuBar* menuBarQ; + std::vector> subMenus; + + MenuBarImpQt() { + // The QMenuBar is deleted by the QMainWindow its attached to. + menuBarQ = new QMenuBar; + menuBarQ->setNativeMenuBar(false); + } + + std::shared_ptr AddSubMenu(const std::string& label) override { + std::shared_ptr menu = std::make_shared(); + menuBarQ->addMenu(menu->menuQ); + menu->menuQ->setTitle(QString::fromStdString(label)); + subMenus.push_back(menu); + return menu; + } + + void Clear() override { + menuBarQ->clear(); + subMenus.clear(); + } + + static void _formActionGroups(QMenu *menu) { + QActionGroup* group = nullptr; + foreach (QAction* act, menu->actions()) { + if(act->menu()) { + _formActionGroups(act->menu()); + } else { + QVariant var = act->property("radio_mark"); + if(var.isValid()) { + if(! group) + group = new QActionGroup(menu); + //printf("radio_mark on menu %p\n", menu); + group->addAction(act); + } else { + group = nullptr; // End current group. + } + } + } + } + + void formActionGroups() { + foreach (QAction* act, menuBarQ->actions()) { + if (act->menu()) { + _formActionGroups(act->menu()); + } + } + } +}; + +MenuBarRef GetOrCreateMainMenu(bool* unique) { + *unique = false; + return std::make_shared(); +} + +//----------------------------------------------------------------------------- +// Windows +//----------------------------------------------------------------------------- + +class SSApplication : public QApplication { +public: + SSApplication(int &argc, char **argv) : QApplication(argc, argv) + { + // Setup fonts once for SSView QLineEdit. +#ifdef _WIN32 + fontEdit = QFont("Arial"); + fontEditMono = QFont("Lucida Console"); +#else + fontEdit = QFontDatabase::systemFont(QFontDatabase::GeneralFont); + fontEditMono = QFontDatabase::systemFont(QFontDatabase::FixedFont); + /* + printf("Font general %s\n", fontEdit.family().toLocal8Bit().data()); + printf("Font mono %s\n", fontEditMono.family().toLocal8Bit().data()); + */ +#endif + + // Set language locale. + std::string localeStr("en_US"); + QStringList list = QLocale::system().uiLanguages(); + if (! list.empty()) { + QString lang = list[0]; + if (lang.size() == 5 && lang[2] == '-') { + lang[2] = '_'; + localeStr = lang.toStdString(); + } + } + SetLocale(localeStr); + setlocale(LC_NUMERIC, "C"); + + QIcon icon(":/icons/solvespace.png"); + setWindowIcon(icon); + } + + QFont fontEdit; + QFont fontEditMono; +}; + +SSView::SSView(QWidget* parent) : QOpenGLWidget(parent) { + entry = new QLineEdit(this); + entry->setVisible(false); + connect(entry, SIGNAL(returnPressed()), SLOT(entryFinished())); + + setFocusPolicy(Qt::FocusPolicy::StrongFocus); + setMouseTracking(true); + pixelRatio = pixelRatioI = 1.0; +} + +void SSView::startEditing(double xd, double yd, int fontHeight, int minWidth, + bool isMonoSpace, const std::string& val) +{ + int x = int(xd / pixelRatio); + int y = int(yd / pixelRatio); + //printf("startEditing %d,%d, %d,%d %d\n", x, y, fontHeight, minWidth, isMonoSpace); + + SSApplication* app = static_cast(qApp); + QFont font = isMonoSpace ? app->fontEditMono : app->fontEdit; + font.setPixelSize(fontHeight); + entry->setFont(font); + + QFontMetrics fm(font); + int valWidth = fm.averageCharWidth() * val.size(); + if (minWidth < valWidth) + minWidth = valWidth; + if (x < 0) + x = 0; + + entry->setGeometry(QRect(x, y - fm.ascent(), minWidth, fm.height())); + entry->setText(QString::fromStdString(val)); + + if (! entry->isVisible()) { + entry->selectAll(); + entry->show(); + entry->setFocus(); + } +} + +void SSView::stopEditing() { + if (entry->isVisible()) { + entry->hide(); + setFocus(); + } +} + +void SSView::entryFinished() { + std::string entryText = entry->text().toStdString(); + receiver->onEditingDone(entryText); +} + +void SSView::paintGL() { + // pixelRatio is set here as devicePixelRatioF has been seen to change + // after the resizeGL() & showEvent() methods are called. + pixelRatio = devicePixelRatioF(); + pixelRatioI = floor(pixelRatio); + + receiver->onRender(); +} + +static inline void setMouseKeyModifiers(MouseEvent& me, const QInputEvent* qe) +{ + Qt::KeyboardModifiers mod = qe->modifiers(); + me.shiftDown = mod & Qt::ShiftModifier ? true : false; + me.controlDown = mod & Qt::ControlModifier ? true : false; +} + +void SSView::wheelEvent(QWheelEvent* event) +{ + const double wheelDelta=120.0; + slvMouseEvent.button = MouseEvent::Button::NONE; + slvMouseEvent.type = MouseEvent::Type::SCROLL_VERT; + slvMouseEvent.scrollDelta = (double)event->angleDelta().y() / wheelDelta; + setMouseKeyModifiers(slvMouseEvent, event); + + receiver->onMouseEvent(slvMouseEvent); +} + +void SSView::updateSlvSpaceMouseEvent(QMouseEvent* event) +{ + switch (event->type()) + { + case QEvent::MouseMove: + slvMouseEvent.type = MouseEvent::Type::MOTION; + break; + case QEvent::MouseButtonPress: + slvMouseEvent.type = MouseEvent::Type::PRESS; + break; + case QEvent::MouseButtonDblClick: + slvMouseEvent.type = MouseEvent::Type::DBL_PRESS; + break; + case QEvent::MouseButtonRelease: + slvMouseEvent.type = MouseEvent::Type::RELEASE; + slvMouseEvent.button = MouseEvent::Button::NONE; + break; + case QEvent::Wheel: + slvMouseEvent.type = MouseEvent::Type::SCROLL_VERT; + //slvMouseEvent.scrollDelta = event-> + break; + case QEvent::Leave: + slvMouseEvent.type = MouseEvent::Type::LEAVE; + slvMouseEvent.button = MouseEvent::Button::NONE; + break; + default: + slvMouseEvent.type = MouseEvent::Type::RELEASE; + slvMouseEvent.button = MouseEvent::Button::NONE; + } + + Qt::MouseButtons buttonState = (slvMouseEvent.type == MouseEvent::Type::MOTION) ? event->buttons() : event->button(); + + switch (buttonState) + { + case Qt::MouseButton::LeftButton: + slvMouseEvent.button = MouseEvent::Button::LEFT; + break; + case Qt::MouseButton::RightButton: + slvMouseEvent.button = MouseEvent::Button::RIGHT; + break; + case Qt::MouseButton::MiddleButton: + slvMouseEvent.button = MouseEvent::Button::MIDDLE; + break; + default: + slvMouseEvent.button = MouseEvent::Button::NONE; + //slvMouseEvent.button = slvMouseEvent.button; + } + +#if QT_VERSION >= 0x060000 + QPointF pos = event->position(); + slvMouseEvent.x = pos.x() * pixelRatio; + slvMouseEvent.y = pos.y() * pixelRatio; +#else + slvMouseEvent.x = double(event->x()); + slvMouseEvent.y = double(event->y()); +#endif + + setMouseKeyModifiers(slvMouseEvent, event); + + receiver->onMouseEvent(slvMouseEvent); +} + +void SSView::updateSlvSpaceKeyEvent(QKeyEvent* event) +{ + slvKeyEvent.key = KeyboardEvent::Key(-1); + + Qt::KeyboardModifiers mod = event->modifiers(); + slvKeyEvent.shiftDown = mod & Qt::ShiftModifier ? true : false; + slvKeyEvent.controlDown = mod & Qt::ControlModifier ? true : false; + + if (event->key() >= Qt::Key_F1 && event->key() <= Qt::Key_F35) { + slvKeyEvent.key = KeyboardEvent::Key::FUNCTION; + slvKeyEvent.num = event->key() - Qt::Key_F1 + 1; + } + if (event->key() >= Qt::Key_Space && event->key() <= Qt::Key_yen) { + slvKeyEvent.key = KeyboardEvent::Key::CHARACTER; + QString keyLower = event->text().toLower(); + slvKeyEvent.chr = keyLower.toUcs4().at(0); + } + + receiver->onKeyboardEvent(slvKeyEvent); +} + +SSTextWindow::SSTextWindow(QWidget* parent) + : QDockWidget(parent) +{ + ssView = new SSView; + + QWidget* group = new QWidget; + scrollBar = new QScrollBar(Qt::Vertical, group); + connect(scrollBar, SIGNAL(valueChanged(int)), SLOT(sliderSlot(int))); + + QHBoxLayout* lo = new QHBoxLayout(group); + lo->setContentsMargins(0, 0, 0, 0); + lo->setSpacing(2); + lo->addWidget(ssView); + lo->addWidget(scrollBar); + group->setLayout(lo); + + setWidget(group); + setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); +} + +void SSTextWindow::sliderSlot(int value) +{ + std::function adjust = ssView->receiver->onScrollbarAdjusted; + if(adjust) + adjust(double(value)); +} + +void SSTextWindow::closeEvent(QCloseEvent* ev) +{ + ssView->receiver->onClose(); + ev->ignore(); +} + +SSMainWindow::SSMainWindow() +{ + ssView = new SSView; + setCentralWidget(ssView); +} + +void SSMainWindow::closeEvent(QCloseEvent* ev) +{ + ssView->receiver->onClose(); + ev->ignore(); +} + +class WindowImplQt final : public Window { +public: + QWidget* ssWindow; // SSMainWindow or SSTextWindow. + SSView* view; // Copy of ssWindow ssView. + + // NOTE: Only TextWindow has a scroll bar and only GraphicsWindow has menus, + // but Platform::Window squishes both together in a single interface. + + QScrollBar* scrollBar; // Copy of SSTextWindow::scrollBar or nullptr. + std::shared_ptr menuBar; + + WindowImplQt(Window::Kind kind, std::shared_ptr parent) + { + if(kind == Platform::Window::Kind::TOOL) { + QWidget* qparent = parent ? parent->ssWindow : nullptr; + + SSTextWindow* twin = new SSTextWindow(qparent); + ssWindow = twin; + scrollBar = twin->scrollBar; + view = twin->ssView; + + // Set object name for saveState settings. + twin->setObjectName("TextWindow"); + + QMainWindow* mwin = qobject_cast(qparent); + if(mwin) { + mwin->addDockWidget(Qt::RightDockWidgetArea, twin); + } + } else { + SSMainWindow* mwin = new SSMainWindow; + ssWindow = mwin; + scrollBar = nullptr; + view = mwin->ssView; + } + + // Have the view call our Platform::Window event methods. + view->receiver = this; + } + + // Returns physical display DPI. + double GetPixelDensity() override { + return QGuiApplication::primaryScreen()->logicalDotsPerInch(); + } + + // Returns raster graphics and coordinate scale (already applied on the platform side), + // i.e. size of logical pixel in physical pixels, or device pixel ratio. + double GetDevicePixelRatio() override { + return view->pixelRatioI; + } + + // Returns (fractional) font scale, to be applied on top of (integral) device pixel ratio. + double GetDeviceFontScale() { + return GetPixelDensity() / GetDevicePixelRatio() / 96.0; + } + + bool IsVisible() override { + return ssWindow->isVisible(); + } + + void SetVisible(bool visible) override { + ssWindow->setVisible(visible); + ssWindow->raise(); + } + + void Focus() override { + ssWindow->setFocus(); + } + + bool IsFullScreen() override { + return ssWindow->isFullScreen(); + } + + void SetFullScreen(bool fullScreen) override { + // Qt::WindowNoState is "normal". + ssWindow->setWindowState(fullScreen ? Qt::WindowFullScreen + : Qt::WindowNoState); + } + + void SetTitle(const std::string& title) override { + ssWindow->setWindowTitle(QString::fromStdString(title)); + } + + void SetMenuBar(MenuBarRef menus) override { + QMainWindow* mwin = qobject_cast(ssWindow); + if (mwin) { + // We must take ownership of the new menus. + menuBar = std::static_pointer_cast(menus); + menuBar->formActionGroups(); + + mwin->setMenuBar(menuBar->menuBarQ); + } + } + + void GetContentSize(double* width, double* height) override { +#if QT_VERSION >= 0x060000 + double pixelRatio = view->pixelRatio; + QSize rc = view->size(); + *width = rc.width() * pixelRatio; + *height = rc.height() * pixelRatio; + //printf("KR cont %f %d,%d %f,%f\n", pixelRatio, rc.width(), rc.height(), *width, *height); +#else + *width = view->width(); + *height = view->height(); + //printf("KR cont %f,%f\n", *width, *height); +#endif + } + + void SetMinContentSize(double width, double height) override { + ssWindow->resize(int(width), int(height)); + } + + void FreezePosition(SettingsRef settings, const std::string& key) override { + // Geometry is only saved for the main window; the TextWindow dock + // widget is part of the saveState(). + QMainWindow* mwin = qobject_cast(ssWindow); + if (mwin) { + QSettings& qset = std::static_pointer_cast(settings)->qset; + QString qkey = QString::fromStdString(key); + qset.setValue(qkey + "_Geometry", mwin->saveGeometry()); + qset.setValue(qkey + "_State", mwin->saveState()); + } + } + + void ThawPosition(SettingsRef settings, const std::string& key) override { + QMainWindow* mwin = qobject_cast(ssWindow); + if (mwin) { + QSettings& qset = std::static_pointer_cast(settings)->qset; + QString qkey = QString::fromStdString(key); + mwin->restoreGeometry(qset.value(qkey + "_Geometry").toByteArray()); + mwin->restoreState(qset.value(qkey + "_State").toByteArray()); + } + } + + void SetCursor(Cursor cursor) override { + + switch (cursor) + { + case Cursor::POINTER: + ssWindow->setCursor(Qt::ArrowCursor); + break; + case Cursor::HAND: + ssWindow->setCursor(Qt::PointingHandCursor); + break; + default: + ssWindow->setCursor(Qt::ArrowCursor); + } + } + + void SetTooltip(const std::string& text, double x, double y, + double width, double height) override { + view->setToolTip(QString::fromStdString(text)); + } + + bool IsEditorVisible() override { + return view->entry->isVisible(); + } + + void ShowEditor(double x, double y, double fontHeight, double minWidth, + bool isMonospace, const std::string& text) override { + view->startEditing(x, y, fontHeight, minWidth, isMonospace, text); + } + + void HideEditor() override { + view->stopEditing(); + } + + void SetScrollbarVisible(bool visible) override { + //printf("Scroll vis %d\n", int(visible)); + if (scrollBar) + scrollBar->setVisible(visible); + } + + void ConfigureScrollbar(double min, double max, double pageSize) override { + //printf("Scroll bar %f %f %f\n", min, max, pageSize); + if (scrollBar) { + scrollBar->setRange(int(min), int(max - pageSize)); + scrollBar->setPageStep(int(pageSize)); + } + } + + double GetScrollbarPosition() override { + if (scrollBar) + return (double) scrollBar->value(); + return 0.0; + } + + void SetScrollbarPosition(double pos) override { + //printf("Scroll pos %f\n", pos); + if (scrollBar) + scrollBar->setValue((int) pos); + } + + void Invalidate() override { + view->update(); + } +}; + +#ifdef WIN32 +// This interferes with our identifier. +# undef CreateWindow +#endif + +WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { + return std::make_shared(kind, + std::static_pointer_cast(parentWindow)); +} + +MessageDialogRef CreateMessageDialog(WindowRef parentWindow) { + return std::make_shared( + std::static_pointer_cast(parentWindow)->ssWindow); +} + +FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) { + return std::make_shared( + std::static_pointer_cast(parentWindow)->ssWindow, + false); +} + +FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) { + return std::make_shared( + std::static_pointer_cast(parentWindow)->ssWindow, + true); +} + +//----------------------------------------------------------------------------- +// 3DConnexion support +//----------------------------------------------------------------------------- + +void Open3DConnexion() {} +void Close3DConnexion() {} +void Request3DConnexionEventsForWindow(WindowRef window) {} + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +std::vector GetFontFiles() { + std::vector fonts; + +#ifdef WIN32 + std::wstring fontsDirW(MAX_PATH, '\0'); + fontsDirW.resize(GetWindowsDirectoryW(&fontsDirW[0], fontsDirW.length())); + fontsDirW += L"\\fonts\\"; + Platform::Path fontsDir = Platform::Path::From(SolveSpace::Platform::Narrow(fontsDirW)); + + WIN32_FIND_DATAW wfd; + HANDLE h = FindFirstFileW((fontsDirW + L"*").c_str(), &wfd); + while (h != INVALID_HANDLE_VALUE) { + fonts.push_back(fontsDir.Join(SolveSpace::Platform::Narrow(wfd.cFileName))); + if (!FindNextFileW(h, &wfd)) break; + } +#else + // FcInit() should have already been called by QFontconfigDatabase. + FcPattern* pat = FcPatternCreate(); + FcObjectSet* os = FcObjectSetBuild(FC_FILE, (char*)0); + FcFontSet* fs = FcFontList(0, pat, os); + + for (int i = 0; i < fs->nfont; i++) { + FcChar8* filenameFC = FcPatternFormat(fs->fonts[i], (const FcChar8*)"%{file}"); + fonts.push_back(Platform::Path::From((const char*)filenameFC)); + FcStrFree(filenameFC); + } + + FcFontSetDestroy(fs); + FcObjectSetDestroy(os); + FcPatternDestroy(pat); +#endif + + return fonts; +} + +void OpenInBrowser(const std::string& url) { + + std::string urlTemp = url; + + if (urlTemp.find("http") == std::string::npos) + { + urlTemp = QApplication::applicationDirPath().toStdString(); + urlTemp += "/" + url; + } + + if (false == QDesktopServices::openUrl(QUrl(urlTemp.c_str(), QUrl::TolerantMode))) + { + std::string errorStr = "Error: unable to open URL path : " + urlTemp; + QMessageBox::critical(0,QString("Manual Open Error"),QString::fromStdString(errorStr),QMessageBox::Ok); + } +} + +void ExitGui() { + // Using exit() rather than quit() to avoid calling window closeEvent() methods. + qApp->exit(); +} + +} +} + +using namespace SolveSpace; + +int main(int argc, char** argv) { + // Must share GL contexts or an undocked QDockWidget is black. + // See https://bugreports.qt.io/browse/QTBUG-89812 + QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts, true); + +#ifndef WIN32 // Allows using OpenGL 1 on Windows + // Specify the GL version so Qt doesn't use GLES on Wayland, which + // doesn't handle "#version 120" shaders. + { + QSurfaceFormat fmt; + fmt.setRenderableType(QSurfaceFormat::OpenGL); + fmt.setVersion(3, 2); + fmt.setProfile(QSurfaceFormat::CoreProfile); + QSurfaceFormat::setDefaultFormat(fmt); + } +#endif + + Platform::SSApplication app(argc, argv); + Platform::Open3DConnexion(); + SS.Init(); + + if(argc >= 2) { + if(argc > 2) { + dbp("Only the first file passed on command line will be opened."); + } + SS.Load(Platform::Path::From(argv[1])); + } + + app.exec(); + + Platform::Close3DConnexion(); + SS.Clear(); + SK.Clear(); + return 0; +} diff --git a/src/platform/guiqt.h b/src/platform/guiqt.h new file mode 100644 index 0000000..b34d883 --- /dev/null +++ b/src/platform/guiqt.h @@ -0,0 +1,132 @@ +//----------------------------------------------------------------------------- +// The Qt-based implementation of platform-dependent GUI functionality. +// +// Copyright 2024 Karl Robillard +// Copyright 2023 Raed Marwan +// SPDX-License-Identifier: GPL-3.0-or-later +//----------------------------------------------------------------------------- + +#ifndef SOLVESPACE_GUIQT_H +#define SOLVESPACE_GUIQT_H + +#include + +#include +#include +#include +#include +#include +#include + + +namespace SolveSpace { +namespace Platform { + +class SSView : public QOpenGLWidget +{ + Q_OBJECT + +public: + SSView(QWidget* parent = 0); + + void startEditing(double xd, double yd, int fontHeight, int minWidth, + bool isMonoSpace, const std::string& val); + void stopEditing(); + + QSize minimumSizeHint() const { + return QSize(50, 50); + } + + QSize sizeHint() const { + return QSize(400, 400); + } + +protected slots: + void entryFinished(); + +protected: + //void initializeGL() {} + //void resizeGL(int width, int height) {} + + void paintGL(); + + //----Mouse Events-------- + void wheelEvent(QWheelEvent* event); + + void mouseReleaseEvent(QMouseEvent* event) override{ + updateSlvSpaceMouseEvent(event); + } + + void mousePressEvent(QMouseEvent* event) override{ + updateSlvSpaceMouseEvent(event); + } + + void mouseMoveEvent(QMouseEvent* event) override{ + updateSlvSpaceMouseEvent(event); + } + + void updateSlvSpaceMouseEvent(QMouseEvent* event); + + //----Keyboard Events-------- + + void keyPressEvent(QKeyEvent* keyEvent) + { + slvKeyEvent.type = KeyboardEvent::Type::PRESS; + updateSlvSpaceKeyEvent(keyEvent); + QWidget::keyPressEvent(keyEvent); + } + + void keyReleaseEvent(QKeyEvent* keyEvent) + { + slvKeyEvent.type = KeyboardEvent::Type::RELEASE; + updateSlvSpaceKeyEvent(keyEvent); + QWidget::keyReleaseEvent(keyEvent); + } + + void updateSlvSpaceKeyEvent(QKeyEvent* event); + +public: + Platform::Window* receiver; + QLineEdit* entry; + double pixelRatio; + double pixelRatioI; + MouseEvent slvMouseEvent; + KeyboardEvent slvKeyEvent; +}; + +class SSTextWindow : public QDockWidget +{ + Q_OBJECT + +public: + SSTextWindow(QWidget* parent = 0); + +protected slots: + void sliderSlot(int value); + +protected: + void closeEvent(QCloseEvent* ev); + +public: + SSView* ssView; + QScrollBar* scrollBar; +}; + +class SSMainWindow : public QMainWindow +{ + Q_OBJECT + +public: + SSMainWindow(); + +protected: + void closeEvent(QCloseEvent* ev); + +public: + SSView* ssView; +}; + +} +} + +#endif diff --git a/src/platform/guiwin.cpp b/src/platform/guiwin.cpp new file mode 100644 index 0000000..4016181 --- /dev/null +++ b/src/platform/guiwin.cpp @@ -0,0 +1,1740 @@ +//----------------------------------------------------------------------------- +// The Win32-based implementation of platform-dependent GUI functionality. +// +// Copyright 2018 whitequark +//----------------------------------------------------------------------------- +#include "config.h" +#include "solvespace.h" +// Include after solvespace.h to avoid identifier clashes. +#include +#include +#include +#include +#include + +// Macros to compile under XP +#if !defined(LSTATUS) +# define LSTATUS LONG +#endif + +#if !defined(MAPVK_VK_TO_CHAR) +# define MAPVK_VK_TO_CHAR 2 +#endif + +#if !defined(USER_DEFAULT_SCREEN_DPI) +# define USER_DEFAULT_SCREEN_DPI 96 +#endif + +#if !defined(TTM_POPUP) +# define TTM_POPUP (WM_USER + 34) +#endif +// End macros to compile under XP + +#if !defined(WM_DPICHANGED) +# define WM_DPICHANGED 0x02E0 +#endif + +// These interfere with our identifiers. +#undef CreateWindow +#undef ERROR + +#if HAVE_OPENGL == 3 +# define EGLAPI /*static linkage*/ +# define EGL_EGLEXT_PROTOTYPES +# include +# include +#endif + +#if defined(HAVE_SPACEWARE) +# include +# include +# undef uint32_t +#endif + +#if defined(__GNUC__) +// Disable bogus warning emitted by GCC on GetProcAddress, since there seems to be no way +// of restructuring the code to easily disable it just at the call site. +#pragma GCC diagnostic ignored "-Wcast-function-type" +#endif + +namespace SolveSpace { +namespace Platform { + +//----------------------------------------------------------------------------- +// Windows API bridging +//----------------------------------------------------------------------------- + +#define sscheck(expr) do { \ + SetLastError(0); \ + if(!(expr)) \ + CheckLastError(__FILE__, __LINE__, __func__, #expr); \ + } while(0) + +void CheckLastError(const char *file, int line, const char *function, const char *expr) { + if(GetLastError() != S_OK) { + LPWSTR messageW; + FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM, + NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPWSTR)&messageW, 0, NULL); + + std::string message; + message += ssprintf("File %s, line %u, function %s:\n", file, line, function); + message += ssprintf("Win32 API call failed: %s.\n", expr); + message += ssprintf("Error: %s", Narrow(messageW).c_str()); + FatalError(message); + } +} + +typedef UINT (WINAPI *LPFNGETDPIFORWINDOW)(HWND); + +UINT ssGetDpiForWindow(HWND hwnd) { + static bool checked; + static LPFNGETDPIFORWINDOW lpfnGetDpiForWindow; + if(!checked) { + checked = true; + lpfnGetDpiForWindow = (LPFNGETDPIFORWINDOW) + GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetDpiForWindow"); + } + if(lpfnGetDpiForWindow) { + return lpfnGetDpiForWindow(hwnd); + } else { + HDC hDc; + sscheck(hDc = GetDC(HWND_DESKTOP)); + UINT dpi; + sscheck(dpi = GetDeviceCaps(hDc, LOGPIXELSX)); + sscheck(ReleaseDC(HWND_DESKTOP, hDc)); + return dpi; + } +} + +typedef BOOL (WINAPI *LPFNADJUSTWINDOWRECTEXFORDPI)(LPRECT, DWORD, BOOL, DWORD, UINT); + +BOOL ssAdjustWindowRectExForDpi(LPRECT lpRect, DWORD dwStyle, BOOL bMenu, + DWORD dwExStyle, UINT dpi) { + static bool checked; + static LPFNADJUSTWINDOWRECTEXFORDPI lpfnAdjustWindowRectExForDpi; + if(!checked) { + checked = true; + lpfnAdjustWindowRectExForDpi = (LPFNADJUSTWINDOWRECTEXFORDPI) + GetProcAddress(GetModuleHandleW(L"user32.dll"), "AdjustWindowRectExForDpi"); + } + if(lpfnAdjustWindowRectExForDpi) { + return lpfnAdjustWindowRectExForDpi(lpRect, dwStyle, bMenu, dwExStyle, dpi); + } else { + return AdjustWindowRectEx(lpRect, dwStyle, bMenu, dwExStyle); + } +} + +//----------------------------------------------------------------------------- +// Utility functions +//----------------------------------------------------------------------------- + +static std::wstring PrepareTitle(const std::string &s) { + return Widen("SolveSpace - " + s); +} + +static std::string NegateMnemonics(const std::string &label) { + std::string newLabel; + for(char c : label) { + newLabel.push_back(c); + if(c == '&') newLabel.push_back(c); + } + return newLabel; +} + +static int Clamp(int x, int a, int b, int brda, int brdb) { + // If we are outside of an edge of the monitor + // and a "border" is requested "move in" from that edge + // by "b/brdX" (the "b" parameter is the resolution) + if((x <= a) && (brda)) { + a += b / brda; // yes "b/brda" since b is the size + } + + if(((x >= b) && brdb)) { + b -= b / brdb; + } + + return max(a, min(x, b)); +} + +//----------------------------------------------------------------------------- +// Fatal errors +//----------------------------------------------------------------------------- + +bool handlingFatalError = false; + +void FatalError(const std::string &message) { + // Indicate that we're handling a fatal error, to avoid re-entering application code + // and potentially crashing even harder. + handlingFatalError = true; + + switch(MessageBoxW(NULL, Platform::Widen(message + "\nGenerate debug report?").c_str(), + L"Fatal error — SolveSpace", + MB_ICONERROR|MB_TASKMODAL|MB_SETFOREGROUND|MB_TOPMOST| + MB_OKCANCEL|MB_DEFBUTTON2)) { + case IDOK: + abort(); + + case IDCANCEL: + default: { + WCHAR appPath[MAX_PATH] = {}; + GetModuleFileNameW(NULL, appPath, sizeof(appPath)); + ShellExecuteW(NULL, L"open", appPath, NULL, NULL, SW_SHOW); + _exit(1); + } + } +} +//----------------------------------------------------------------------------- +// Settings +//----------------------------------------------------------------------------- + +class SettingsImplWin32 final : public Settings { +public: + HKEY hKey = NULL; + + HKEY GetKey() { + if(hKey == NULL) { + sscheck(ERROR_SUCCESS == RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\SolveSpace", 0, NULL, 0, + KEY_ALL_ACCESS, NULL, &hKey, NULL)); + } + return hKey; + } + + ~SettingsImplWin32() { + if(hKey != NULL) { + sscheck(ERROR_SUCCESS == RegCloseKey(hKey)); + } + } + + void FreezeInt(const std::string &key, uint32_t value) { + sscheck(ERROR_SUCCESS == RegSetValueExW(GetKey(), &Widen(key)[0], 0, + REG_DWORD, (const BYTE *)&value, sizeof(value))); + } + + uint32_t ThawInt(const std::string &key, uint32_t defaultValue) { + DWORD value; + DWORD type, length = sizeof(value); + LSTATUS result = RegQueryValueExW(GetKey(), &Widen(key)[0], 0, + &type, (BYTE *)&value, &length); + if(result == ERROR_SUCCESS && type == REG_DWORD) { + return value; + } + return defaultValue; + } + + void FreezeFloat(const std::string &key, double value) { + sscheck(ERROR_SUCCESS == RegSetValueExW(GetKey(), &Widen(key)[0], 0, + REG_QWORD, (const BYTE *)&value, sizeof(value))); + } + + double ThawFloat(const std::string &key, double defaultValue) { + double value; + DWORD type, length = sizeof(value); + LSTATUS result = RegQueryValueExW(GetKey(), &Widen(key)[0], 0, + &type, (BYTE *)&value, &length); + if(result == ERROR_SUCCESS && type == REG_QWORD) { + return value; + } + return defaultValue; + } + + void FreezeString(const std::string &key, const std::string &value) { + ssassert(value.length() == strlen(value.c_str()), + "illegal null byte in middle of a string setting"); + std::wstring valueW = Widen(value); + sscheck(ERROR_SUCCESS == RegSetValueExW(GetKey(), &Widen(key)[0], 0, + REG_SZ, (const BYTE *)&valueW[0], (valueW.length() + 1) * 2)); + } + + std::string ThawString(const std::string &key, const std::string &defaultValue) { + DWORD type, length = 0; + LSTATUS result = RegQueryValueExW(GetKey(), &Widen(key)[0], 0, + &type, NULL, &length); + if(result == ERROR_SUCCESS && type == REG_SZ) { + std::wstring valueW; + valueW.resize(length / 2 - 1); + sscheck(ERROR_SUCCESS == RegQueryValueExW(GetKey(), &Widen(key)[0], 0, + &type, (BYTE *)&valueW[0], &length)); + return Narrow(valueW); + } + return defaultValue; + } +}; + +SettingsRef GetSettings() { + return std::make_shared(); +} + +//----------------------------------------------------------------------------- +// Timers +//----------------------------------------------------------------------------- + +class TimerImplWin32 final : public Timer { +public: + static HWND WindowHandle() { + static HWND hTimerWnd; + if(hTimerWnd == NULL) { + sscheck(hTimerWnd = CreateWindowExW(0, L"Message", NULL, 0, 0, 0, 0, 0, + HWND_MESSAGE, NULL, NULL, NULL)); + } + return hTimerWnd; + } + + static void CALLBACK TimerFunc(HWND hwnd, UINT msg, UINT_PTR event, DWORD time) { + sscheck(KillTimer(WindowHandle(), event)); + + TimerImplWin32 *timer = (TimerImplWin32*)event; + if(timer->onTimeout) { + timer->onTimeout(); + } + } + + void RunAfter(unsigned milliseconds) override { + // FIXME(platform/gui): use SetCoalescableTimer when it's available (8+) + sscheck(SetTimer(WindowHandle(), (UINT_PTR)this, + milliseconds, &TimerImplWin32::TimerFunc)); + } + + ~TimerImplWin32() { + // FIXME(platform/gui): there's a race condition here--WM_TIMER messages already + // posted to the queue are not removed, so this destructor is at most "best effort". + KillTimer(WindowHandle(), (UINT_PTR)this); + } +}; + +TimerRef CreateTimer() { + return std::make_shared(); +} + +//----------------------------------------------------------------------------- +// Menus +//----------------------------------------------------------------------------- + +class MenuImplWin32; + +class MenuItemImplWin32 final : public MenuItem { +public: + std::shared_ptr menu; + + HMENU Handle(); + + MENUITEMINFOW GetInfo(UINT mask) { + MENUITEMINFOW mii = {}; + mii.cbSize = sizeof(mii); + mii.fMask = mask; + sscheck(GetMenuItemInfoW(Handle(), (UINT_PTR)this, FALSE, &mii)); + return mii; + } + + void SetAccelerator(KeyboardEvent accel) override { + MENUITEMINFOW mii = GetInfo(MIIM_TYPE); + + std::wstring nameW(mii.cch, L'\0'); + mii.dwTypeData = &nameW[0]; + mii.cch++; + sscheck(GetMenuItemInfoW(Handle(), (UINT_PTR)this, FALSE, &mii)); + + std::string name = Narrow(nameW); + if(name.find('\t') != std::string::npos) { + name = name.substr(0, name.find('\t')); + } + name += '\t'; + name += AcceleratorDescription(accel); + + nameW = Widen(name); + mii.fMask = MIIM_STRING; + mii.dwTypeData = &nameW[0]; + sscheck(SetMenuItemInfoW(Handle(), (UINT_PTR)this, FALSE, &mii)); + } + + void SetIndicator(Indicator type) override { + MENUITEMINFOW mii = GetInfo(MIIM_FTYPE); + switch(type) { + case Indicator::NONE: + case Indicator::CHECK_MARK: + mii.fType &= ~MFT_RADIOCHECK; + break; + + case Indicator::RADIO_MARK: + mii.fType |= MFT_RADIOCHECK; + break; + } + sscheck(SetMenuItemInfoW(Handle(), (UINT_PTR)this, FALSE, &mii)); + } + + void SetActive(bool active) override { + MENUITEMINFOW mii = GetInfo(MIIM_STATE); + if(active) { + mii.fState |= MFS_CHECKED; + } else { + mii.fState &= ~MFS_CHECKED; + } + sscheck(SetMenuItemInfoW(Handle(), (UINT_PTR)this, FALSE, &mii)); + } + + void SetEnabled(bool enabled) override { + MENUITEMINFOW mii = GetInfo(MIIM_STATE); + if(enabled) { + mii.fState &= ~(MFS_DISABLED|MFS_GRAYED); + } else { + mii.fState |= MFS_DISABLED|MFS_GRAYED; + } + sscheck(SetMenuItemInfoW(Handle(), (UINT_PTR)this, FALSE, &mii)); + } +}; + +int64_t contextMenuPopTime = 0; + +class MenuImplWin32 final : public Menu { +public: + HMENU hMenu; + + std::weak_ptr weakThis; + std::vector> menuItems; + std::vector> subMenus; + + MenuImplWin32() { + sscheck(hMenu = CreatePopupMenu()); + } + + MenuItemRef AddItem(const std::string &label, + std::function onTrigger = NULL, + bool mnemonics = true) override { + auto menuItem = std::make_shared(); + menuItem->menu = weakThis.lock(); + menuItem->onTrigger = onTrigger; + menuItems.push_back(menuItem); + + sscheck(AppendMenuW(hMenu, MF_STRING, (UINT_PTR)menuItem.get(), + Widen(mnemonics ? label : NegateMnemonics(label)).c_str())); + + // uID is just an UINT, which isn't large enough to hold a pointer on 64-bit Windows, + // so we use dwItemData, which is. + MENUITEMINFOW mii = {}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_DATA; + mii.dwItemData = (LONG_PTR)menuItem.get(); + sscheck(SetMenuItemInfoW(hMenu, (UINT_PTR)menuItem.get(), FALSE, &mii)); + + return menuItem; + } + + MenuRef AddSubMenu(const std::string &label) override { + auto subMenu = std::make_shared(); + subMenu->weakThis = subMenu; + subMenus.push_back(subMenu); + + sscheck(AppendMenuW(hMenu, MF_STRING|MF_POPUP, + (UINT_PTR)subMenu->hMenu, Widen(label).c_str())); + + return subMenu; + } + + void AddSeparator() override { + sscheck(AppendMenuW(hMenu, MF_SEPARATOR, 0, L"")); + } + + void PopUp() override { + MENUINFO mi = {}; + mi.cbSize = sizeof(mi); + mi.fMask = MIM_APPLYTOSUBMENUS|MIM_STYLE; + mi.dwStyle = MNS_NOTIFYBYPOS; + sscheck(SetMenuInfo(hMenu, &mi)); + + POINT pt; + sscheck(GetCursorPos(&pt)); + + sscheck(TrackPopupMenu(hMenu, TPM_TOPALIGN, pt.x, pt.y, 0, GetActiveWindow(), NULL)); + contextMenuPopTime = GetMilliseconds(); + } + + void Clear() override { + for(int n = GetMenuItemCount(hMenu) - 1; n >= 0; n--) { + sscheck(RemoveMenu(hMenu, n, MF_BYPOSITION)); + } + menuItems.clear(); + subMenus.clear(); + } + + ~MenuImplWin32() { + Clear(); + sscheck(DestroyMenu(hMenu)); + } +}; + +HMENU MenuItemImplWin32::Handle() { + return menu->hMenu; +} + +MenuRef CreateMenu() { + auto menu = std::make_shared(); + // std::enable_shared_from_this fails for some reason, not sure why + menu->weakThis = menu; + return menu; +} + +class MenuBarImplWin32 final : public MenuBar { +public: + HMENU hMenuBar; + + std::vector> subMenus; + + MenuBarImplWin32() { + sscheck(hMenuBar = ::CreateMenu()); + } + + MenuRef AddSubMenu(const std::string &label) override { + auto subMenu = std::make_shared(); + subMenu->weakThis = subMenu; + subMenus.push_back(subMenu); + + sscheck(AppendMenuW(hMenuBar, MF_STRING|MF_POPUP, + (UINT_PTR)subMenu->hMenu, Widen(label).c_str())); + + return subMenu; + } + + void Clear() override { + for(int n = GetMenuItemCount(hMenuBar) - 1; n >= 0; n--) { + sscheck(RemoveMenu(hMenuBar, n, MF_BYPOSITION)); + } + subMenus.clear(); + } + + ~MenuBarImplWin32() { + Clear(); + sscheck(DestroyMenu(hMenuBar)); + } +}; + +MenuBarRef GetOrCreateMainMenu(bool *unique) { + *unique = false; + return std::make_shared(); +} + +//----------------------------------------------------------------------------- +// Windows +//----------------------------------------------------------------------------- + +#define SCROLLBAR_UNIT 65536 + +class WindowImplWin32 final : public Window { +public: + HWND hWindow = NULL; + HWND hTooltip = NULL; + HWND hEditor = NULL; + WNDPROC editorWndProc = NULL; + +#if HAVE_OPENGL == 1 + HGLRC hGlRc = NULL; +#elif HAVE_OPENGL == 3 + static EGLDisplay eglDisplay; + EGLSurface eglSurface = EGL_NO_SURFACE; + EGLContext eglContext = EGL_NO_CONTEXT; +#endif + + WINDOWPLACEMENT placement = {}; + int minWidth = 0, minHeight = 0; + +#if defined(HAVE_SPACEWARE) + SiOpenData sod = {}; + SiHdl hSpaceWare = SI_NO_HANDLE; +#endif + + std::shared_ptr menuBar; + std::string tooltipText; + bool scrollbarVisible = false; + + static void RegisterWindowClass() { + static bool registered; + if(registered) return; + + WNDCLASSEXW wc = {}; + wc.cbSize = sizeof(wc); + wc.style = CS_BYTEALIGNCLIENT|CS_BYTEALIGNWINDOW|CS_OWNDC|CS_DBLCLKS; + wc.lpfnWndProc = WndProc; + wc.cbWndExtra = sizeof(WindowImplWin32 *); + wc.hIcon = (HICON)LoadImage(GetModuleHandle(NULL), MAKEINTRESOURCE(4000), + IMAGE_ICON, 32, 32, 0); + wc.hIconSm = (HICON)LoadImage(GetModuleHandle(NULL), MAKEINTRESOURCE(4000), + IMAGE_ICON, 16, 16, 0); + wc.hCursor = LoadCursorW(NULL, IDC_ARROW); + wc.lpszClassName = L"SolveSpace"; + sscheck(RegisterClassExW(&wc)); + registered = true; + } + + WindowImplWin32(Window::Kind kind, std::shared_ptr parentWindow) { + placement.length = sizeof(placement); + + RegisterWindowClass(); + + HWND hParentWindow = NULL; + if(parentWindow) { + hParentWindow = parentWindow->hWindow; + } + + DWORD style = WS_SIZEBOX|WS_CLIPCHILDREN; + switch(kind) { + case Window::Kind::TOPLEVEL: + style |= WS_OVERLAPPEDWINDOW|WS_CLIPSIBLINGS; + break; + + case Window::Kind::TOOL: + style |= WS_POPUPWINDOW|WS_CAPTION; + break; + } + sscheck(hWindow = CreateWindowExW(0, L"SolveSpace", L"", style, + CW_USEDEFAULT, CW_USEDEFAULT, + CW_USEDEFAULT, CW_USEDEFAULT, + hParentWindow, NULL, NULL, NULL)); + sscheck(SetWindowLongPtr(hWindow, 0, (LONG_PTR)this)); + + sscheck(hTooltip = CreateWindowExW(0, TOOLTIPS_CLASS, NULL, + WS_POPUP|TTS_NOPREFIX|TTS_ALWAYSTIP, + CW_USEDEFAULT, CW_USEDEFAULT, + CW_USEDEFAULT, CW_USEDEFAULT, + hWindow, NULL, NULL, NULL)); + sscheck(SetWindowPos(hTooltip, HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE)); + + TOOLINFOW ti = {}; + ti.cbSize = sizeof(ti); + ti.uFlags = TTF_SUBCLASS; + ti.hwnd = hWindow; + ti.lpszText = (LPWSTR)L""; + sscheck(SendMessageW(hTooltip, TTM_ADDTOOLW, 0, (LPARAM)&ti)); + sscheck(SendMessageW(hTooltip, TTM_ACTIVATE, FALSE, 0)); + + DWORD editorStyle = WS_CLIPSIBLINGS|WS_CHILD|WS_TABSTOP|ES_AUTOHSCROLL; + sscheck(hEditor = CreateWindowExW(WS_EX_CLIENTEDGE, WC_EDIT, L"", editorStyle, + 0, 0, 0, 0, hWindow, NULL, NULL, NULL)); + sscheck(editorWndProc = + (WNDPROC)SetWindowLongPtr(hEditor, GWLP_WNDPROC, (LONG_PTR)EditorWndProc)); + + HDC hDc; + sscheck(hDc = GetDC(hWindow)); + +#if HAVE_OPENGL == 1 + PIXELFORMATDESCRIPTOR pfd = {}; + pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR); + pfd.nVersion = 1; + pfd.dwFlags = PFD_DRAW_TO_WINDOW|PFD_SUPPORT_OPENGL|PFD_DOUBLEBUFFER; + pfd.dwLayerMask = PFD_MAIN_PLANE; + pfd.iPixelType = PFD_TYPE_RGBA; + pfd.cColorBits = 32; + pfd.cDepthBits = 24; + pfd.cAccumBits = 0; + pfd.cStencilBits = 0; + int pixelFormat; + sscheck(pixelFormat = ChoosePixelFormat(hDc, &pfd)); + sscheck(SetPixelFormat(hDc, pixelFormat, &pfd)); + + sscheck(hGlRc = wglCreateContext(hDc)); +#elif HAVE_OPENGL == 3 + if(eglDisplay == EGL_NO_DISPLAY) { + ssassert(eglBindAPI(EGL_OPENGL_ES_API), "Cannot bind EGL API"); + + EGLBoolean initialized = EGL_FALSE; + for(auto &platformType : { + // Try platform types from least to most amount of software translation required. + std::make_pair("OpenGL ES", EGL_PLATFORM_ANGLE_TYPE_OPENGLES_ANGLE), + std::make_pair("OpenGL", EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE), + std::make_pair("Direct3D 11", EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE), + std::make_pair("Direct3D 9", EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE), + }) { + dbp("Initializing ANGLE with %s backend", platformType.first); + EGLint displayAttributes[] = { + EGL_PLATFORM_ANGLE_TYPE_ANGLE, platformType.second, + EGL_NONE + }; + eglDisplay = eglGetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, hDc, + displayAttributes); + if(eglDisplay != EGL_NO_DISPLAY) { + initialized = eglInitialize(eglDisplay, NULL, NULL); + if(initialized) break; + eglTerminate(eglDisplay); + } + } + ssassert(initialized, "Cannot find a suitable EGL display"); + } + + EGLint configAttributes[] = { + EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_DEPTH_SIZE, 24, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_NONE + }; + EGLint numConfigs; + EGLConfig windowConfig; + ssassert(eglChooseConfig(eglDisplay, configAttributes, &windowConfig, 1, &numConfigs), + "Cannot choose EGL configuration"); + + EGLint surfaceAttributes[] = { + EGL_NONE + }; + eglSurface = eglCreateWindowSurface(eglDisplay, windowConfig, hWindow, surfaceAttributes); + ssassert(eglSurface != EGL_NO_SURFACE, "Cannot create EGL window surface"); + + EGLint contextAttributes[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE + }; + eglContext = eglCreateContext(eglDisplay, windowConfig, NULL, contextAttributes); + ssassert(eglContext != EGL_NO_CONTEXT, "Cannot create EGL context"); +#endif + + sscheck(ReleaseDC(hWindow, hDc)); + } + + ~WindowImplWin32() { + // Make sure any of our child windows get destroyed before we call DestroyWindow, or their + // own destructors may fail. + menuBar.reset(); + + sscheck(DestroyWindow(hWindow)); +#if defined(HAVE_SPACEWARE) + if(hSpaceWare != SI_NO_HANDLE) { + SiClose(hSpaceWare); + } +#endif + } + + static LRESULT CALLBACK WndProc(HWND h, UINT msg, WPARAM wParam, LPARAM lParam) { + if(handlingFatalError) return TRUE; + + WindowImplWin32 *window; + sscheck(window = (WindowImplWin32 *)GetWindowLongPtr(h, 0)); + + // The wndproc may be called from within CreateWindowEx, and before we've associated + // the window with the WindowImplWin32. In that case, just defer to the default wndproc. + if(window == NULL) { + return DefWindowProcW(h, msg, wParam, lParam); + } + +#if defined(HAVE_SPACEWARE) + if(window->hSpaceWare != SI_NO_HANDLE) { + SiGetEventData sged; + SiGetEventWinInit(&sged, msg, wParam, lParam); + + SiSpwEvent sse; + if(SiGetEvent(window->hSpaceWare, 0, &sged, &sse) == SI_IS_EVENT) { + SixDofEvent event = {}; + event.shiftDown = ((GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0); + event.controlDown = ((GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0); + if(sse.type == SI_MOTION_EVENT) { + // The Z axis translation and rotation are both + // backwards in the default mapping. + event.type = SixDofEvent::Type::MOTION; + event.translationX = sse.u.spwData.mData[SI_TX]*1.0, + event.translationY = sse.u.spwData.mData[SI_TY]*1.0, + event.translationZ = -sse.u.spwData.mData[SI_TZ]*1.0, + event.rotationX = sse.u.spwData.mData[SI_RX]*0.001, + event.rotationY = sse.u.spwData.mData[SI_RY]*0.001, + event.rotationZ = -sse.u.spwData.mData[SI_RZ]*0.001; + } else if(sse.type == SI_BUTTON_EVENT) { + if(SiButtonPressed(&sse) == SI_APP_FIT_BUTTON) { + event.type = SixDofEvent::Type::PRESS; + event.button = SixDofEvent::Button::FIT; + } + if(SiButtonReleased(&sse) == SI_APP_FIT_BUTTON) { + event.type = SixDofEvent::Type::RELEASE; + event.button = SixDofEvent::Button::FIT; + } + } else { + return 0; + } + if(window->onSixDofEvent) { + window->onSixDofEvent(event); + } + return 0; + } + } +#endif + + switch (msg) { + case WM_ERASEBKGND: + break; + + case WM_PAINT: { + PAINTSTRUCT ps; + HDC hDc = BeginPaint(window->hWindow, &ps); + if(window->onRender) { +#if HAVE_OPENGL == 1 + wglMakeCurrent(hDc, window->hGlRc); +#elif HAVE_OPENGL == 3 + eglMakeCurrent(window->eglDisplay, window->eglSurface, + window->eglSurface, window->eglContext); +#endif + window->onRender(); +#if HAVE_OPENGL == 1 + SwapBuffers(hDc); +#elif HAVE_OPENGL == 3 + eglSwapBuffers(window->eglDisplay, window->eglSurface); + (void)hDc; +#endif + } + EndPaint(window->hWindow, &ps); + break; + } + + case WM_CLOSE: + if(window->onClose) { + window->onClose(); + } + break; + + case WM_SIZE: + window->Invalidate(); + break; + + case WM_SIZING: { + double pixelRatio = window->GetDevicePixelRatio(); + + RECT rcw, rcc; + sscheck(GetWindowRect(window->hWindow, &rcw)); + sscheck(GetClientRect(window->hWindow, &rcc)); + int nonClientWidth = (rcw.right - rcw.left) - (rcc.right - rcc.left); + int nonClientHeight = (rcw.bottom - rcw.top) - (rcc.bottom - rcc.top); + + RECT *rc = (RECT *)lParam; + int adjWidth = rc->right - rc->left; + int adjHeight = rc->bottom - rc->top; + + adjWidth -= nonClientWidth; + adjWidth = max((int)(window->minWidth * pixelRatio), adjWidth); + adjWidth += nonClientWidth; + adjHeight -= nonClientHeight; + adjHeight = max((int)(window->minHeight * pixelRatio), adjHeight); + adjHeight += nonClientHeight; + switch(wParam) { + case WMSZ_RIGHT: + case WMSZ_BOTTOMRIGHT: + case WMSZ_TOPRIGHT: + rc->right = rc->left + adjWidth; + break; + + case WMSZ_LEFT: + case WMSZ_BOTTOMLEFT: + case WMSZ_TOPLEFT: + rc->left = rc->right - adjWidth; + break; + } + switch(wParam) { + case WMSZ_BOTTOM: + case WMSZ_BOTTOMLEFT: + case WMSZ_BOTTOMRIGHT: + rc->bottom = rc->top + adjHeight; + break; + + case WMSZ_TOP: + case WMSZ_TOPLEFT: + case WMSZ_TOPRIGHT: + rc->top = rc->bottom - adjHeight; + break; + } + break; + } + + case WM_DPICHANGED: { + RECT rc = *(RECT *)lParam; + sscheck(SendMessage(window->hWindow, WM_SIZING, WMSZ_BOTTOMRIGHT, (LPARAM)&rc)); + sscheck(SetWindowPos(window->hWindow, NULL, rc.left, rc.top, + rc.right - rc.left, rc.bottom - rc.top, + SWP_NOZORDER|SWP_NOACTIVATE)); + window->Invalidate(); + break; + } + + case WM_LBUTTONDOWN: + case WM_MBUTTONDOWN: + case WM_RBUTTONDOWN: + case WM_LBUTTONDBLCLK: + case WM_MBUTTONDBLCLK: + case WM_RBUTTONDBLCLK: + case WM_LBUTTONUP: + case WM_MBUTTONUP: + case WM_RBUTTONUP: + if(GetMilliseconds() - Platform::contextMenuPopTime < 100) { + // Ignore the mouse click that dismisses a context menu, to avoid + // (e.g.) clearing a selection. + return 0; + } + // fallthrough + case WM_MOUSEMOVE: + case WM_MOUSEWHEEL: + case WM_MOUSELEAVE: { + double pixelRatio = window->GetDevicePixelRatio(); + + MouseEvent event = {}; + event.x = GET_X_LPARAM(lParam) / pixelRatio; + event.y = GET_Y_LPARAM(lParam) / pixelRatio; + event.button = MouseEvent::Button::NONE; + + event.shiftDown = (wParam & MK_SHIFT) != 0; + event.controlDown = (wParam & MK_CONTROL) != 0; + + bool consumed = false; + switch(msg) { + case WM_LBUTTONDOWN: + event.button = MouseEvent::Button::LEFT; + event.type = MouseEvent::Type::PRESS; + break; + case WM_MBUTTONDOWN: + event.button = MouseEvent::Button::MIDDLE; + event.type = MouseEvent::Type::PRESS; + break; + case WM_RBUTTONDOWN: + event.button = MouseEvent::Button::RIGHT; + event.type = MouseEvent::Type::PRESS; + break; + + case WM_LBUTTONDBLCLK: + event.button = MouseEvent::Button::LEFT; + event.type = MouseEvent::Type::DBL_PRESS; + break; + case WM_MBUTTONDBLCLK: + event.button = MouseEvent::Button::MIDDLE; + event.type = MouseEvent::Type::DBL_PRESS; + break; + case WM_RBUTTONDBLCLK: + event.button = MouseEvent::Button::RIGHT; + event.type = MouseEvent::Type::DBL_PRESS; + break; + + case WM_LBUTTONUP: + event.button = MouseEvent::Button::LEFT; + event.type = MouseEvent::Type::RELEASE; + break; + case WM_MBUTTONUP: + event.button = MouseEvent::Button::MIDDLE; + event.type = MouseEvent::Type::RELEASE; + break; + case WM_RBUTTONUP: + event.button = MouseEvent::Button::RIGHT; + event.type = MouseEvent::Type::RELEASE; + break; + + case WM_MOUSEWHEEL: + // Make the mousewheel work according to which window the mouse is + // over, not according to which window is active. + POINT pt; + pt.x = GET_X_LPARAM(lParam); + pt.y = GET_Y_LPARAM(lParam); + HWND hWindowUnderMouse; + sscheck(hWindowUnderMouse = WindowFromPoint(pt)); + if(hWindowUnderMouse && hWindowUnderMouse != h) { + SendMessageW(hWindowUnderMouse, msg, wParam, lParam); + consumed = true; + break; + } + + // Convert the mouse coordinates from screen to client area so that + // scroll wheel zooming remains centered irrespective of the window + // position. + ScreenToClient(hWindowUnderMouse, &pt); + event.x = pt.x / pixelRatio; + event.y = pt.y / pixelRatio; + + event.type = MouseEvent::Type::SCROLL_VERT; + event.scrollDelta = GET_WHEEL_DELTA_WPARAM(wParam) / (double)WHEEL_DELTA; + break; + + case WM_MOUSELEAVE: + event.type = MouseEvent::Type::LEAVE; + break; + case WM_MOUSEMOVE: { + event.type = MouseEvent::Type::MOTION; + + if(wParam & MK_LBUTTON) { + event.button = MouseEvent::Button::LEFT; + } else if(wParam & MK_MBUTTON) { + event.button = MouseEvent::Button::MIDDLE; + } else if(wParam & MK_RBUTTON) { + event.button = MouseEvent::Button::RIGHT; + } + + // We need this in order to get the WM_MOUSELEAVE + TRACKMOUSEEVENT tme = {}; + tme.cbSize = sizeof(tme); + tme.dwFlags = TME_LEAVE; + tme.hwndTrack = window->hWindow; + sscheck(TrackMouseEvent(&tme)); + break; + } + } + + if(!consumed && window->onMouseEvent) { + window->onMouseEvent(event); + } + break; + } + + case WM_KEYDOWN: + case WM_KEYUP: { + Platform::KeyboardEvent event = {}; + if(msg == WM_KEYDOWN) { + event.type = Platform::KeyboardEvent::Type::PRESS; + } else if(msg == WM_KEYUP) { + event.type = Platform::KeyboardEvent::Type::RELEASE; + } + + if(GetKeyState(VK_SHIFT) & 0x8000) + event.shiftDown = true; + if(GetKeyState(VK_CONTROL) & 0x8000) + event.controlDown = true; + + if(wParam >= VK_F1 && wParam <= VK_F12) { + event.key = Platform::KeyboardEvent::Key::FUNCTION; + event.num = wParam - VK_F1 + 1; + } else { + event.key = Platform::KeyboardEvent::Key::CHARACTER; + event.chr = tolower(MapVirtualKeyW(wParam, MAPVK_VK_TO_CHAR)); + if(event.chr == 0) { + if(wParam == VK_DELETE) { + event.chr = '\x7f'; + } else { + // Non-mappable key. + break; + } + } else if(event.chr == '.' && event.shiftDown) { + event.chr = '>'; + event.shiftDown = false;; + } + } + + if(window->onKeyboardEvent) { + window->onKeyboardEvent(event); + } + break; + } + + case WM_SYSKEYDOWN: { + HWND hParent; + sscheck(hParent = GetParent(h)); + if(hParent != NULL) { + // If the user presses the Alt key when a tool window has focus, + // then that should probably go to the main window instead. + sscheck(SetForegroundWindow(hParent)); + break; + } else { + return DefWindowProcW(h, msg, wParam, lParam); + } + } + + case WM_VSCROLL: { + SCROLLINFO si = {}; + si.cbSize = sizeof(si); + si.fMask = SIF_POS|SIF_TRACKPOS|SIF_RANGE|SIF_PAGE; + sscheck(GetScrollInfo(window->hWindow, SB_VERT, &si)); + + switch(LOWORD(wParam)) { + case SB_LINEUP: si.nPos -= SCROLLBAR_UNIT; break; + case SB_PAGEUP: si.nPos -= si.nPage; break; + case SB_LINEDOWN: si.nPos += SCROLLBAR_UNIT; break; + case SB_PAGEDOWN: si.nPos += si.nPage; break; + case SB_TOP: si.nPos = si.nMin; break; + case SB_BOTTOM: si.nPos = si.nMax; break; + case SB_THUMBTRACK: + case SB_THUMBPOSITION: si.nPos = si.nTrackPos; break; + } + + si.nPos = min((UINT)si.nPos, (UINT)(si.nMax - si.nPage)); + + if(window->onScrollbarAdjusted) { + window->onScrollbarAdjusted((double)si.nPos / SCROLLBAR_UNIT); + } + break; + } + + case WM_MENUCOMMAND: { + MENUITEMINFOW mii = {}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_DATA; + sscheck(GetMenuItemInfoW((HMENU)lParam, wParam, TRUE, &mii)); + + MenuItemImplWin32 *menuItem = (MenuItemImplWin32 *)mii.dwItemData; + if(menuItem->onTrigger) { + menuItem->onTrigger(); + } + break; + } + + default: + return DefWindowProcW(h, msg, wParam, lParam); + } + + return 0; + } + + static LRESULT CALLBACK EditorWndProc(HWND h, UINT msg, WPARAM wParam, LPARAM lParam) { + if(handlingFatalError) return 0; + + HWND hWindow; + sscheck(hWindow = GetParent(h)); + + WindowImplWin32 *window; + sscheck(window = (WindowImplWin32 *)GetWindowLongPtr(hWindow, 0)); + + switch(msg) { + case WM_CHAR: + if(wParam == VK_RETURN) { + if(window->onEditingDone) { + int length; + sscheck(length = GetWindowTextLength(h)); + + std::wstring resultW; + resultW.resize(length); + sscheck(GetWindowTextW(h, &resultW[0], resultW.length() + 1)); + + window->onEditingDone(Narrow(resultW)); + return 0; + } + } else if(wParam == VK_ESCAPE) { + window->HideEditor(); + return 0; + } + } + + return CallWindowProc(window->editorWndProc, h, msg, wParam, lParam); + } + + double GetPixelDensity() override { + UINT dpi; + sscheck(dpi = ssGetDpiForWindow(hWindow)); + return (double)dpi; + } + + double GetDevicePixelRatio() override { + UINT dpi; + sscheck(dpi = ssGetDpiForWindow(hWindow)); + return (double)dpi / USER_DEFAULT_SCREEN_DPI; + } + + bool IsVisible() override { + BOOL isVisible; + isVisible = IsWindowVisible(hWindow); + return isVisible == TRUE; + } + + void SetVisible(bool visible) override { + ShowWindow(hWindow, visible ? SW_SHOW : SW_HIDE); + } + + void Focus() override { + sscheck(SetActiveWindow(hWindow)); + } + + bool IsFullScreen() override { + DWORD style; + sscheck(style = GetWindowLongPtr(hWindow, GWL_STYLE)); + return !(style & WS_OVERLAPPEDWINDOW); + } + + void SetFullScreen(bool fullScreen) override { + DWORD style; + sscheck(style = GetWindowLongPtr(hWindow, GWL_STYLE)); + if(fullScreen) { + sscheck(GetWindowPlacement(hWindow, &placement)); + + MONITORINFO mi; + mi.cbSize = sizeof(mi); + sscheck(GetMonitorInfo(MonitorFromWindow(hWindow, MONITOR_DEFAULTTONEAREST), &mi)); + + sscheck(SetWindowLong(hWindow, GWL_STYLE, style & ~WS_OVERLAPPEDWINDOW)); + sscheck(SetWindowPos(hWindow, HWND_NOTOPMOST, + mi.rcMonitor.left, mi.rcMonitor.top, + mi.rcMonitor.right - mi.rcMonitor.left, + mi.rcMonitor.bottom - mi.rcMonitor.top, + SWP_NOOWNERZORDER|SWP_FRAMECHANGED)); + } else { + sscheck(SetWindowLong(hWindow, GWL_STYLE, style | WS_OVERLAPPEDWINDOW)); + sscheck(SetWindowPlacement(hWindow, &placement)); + sscheck(SetWindowPos(hWindow, NULL, 0, 0, 0, 0, + SWP_NOMOVE|SWP_NOSIZE|SWP_NOZORDER| + SWP_NOOWNERZORDER|SWP_FRAMECHANGED)); + } + } + + void SetTitle(const std::string &title) override { + sscheck(SetWindowTextW(hWindow, PrepareTitle(title).c_str())); + } + + void SetMenuBar(MenuBarRef newMenuBar) override { + menuBar = std::static_pointer_cast(newMenuBar); + + MENUINFO mi = {}; + mi.cbSize = sizeof(mi); + mi.fMask = MIM_APPLYTOSUBMENUS|MIM_STYLE; + mi.dwStyle = MNS_NOTIFYBYPOS; + sscheck(SetMenuInfo(menuBar->hMenuBar, &mi)); + + sscheck(SetMenu(hWindow, menuBar->hMenuBar)); + } + + void GetContentSize(double *width, double *height) override { + double pixelRatio = GetDevicePixelRatio(); + + RECT rc; + sscheck(GetClientRect(hWindow, &rc)); + *width = (rc.right - rc.left) / pixelRatio; + *height = (rc.bottom - rc.top) / pixelRatio; + } + + void SetMinContentSize(double width, double height) override { + minWidth = (int)width; + minHeight = (int)height; + + double pixelRatio = GetDevicePixelRatio(); + + RECT rc; + sscheck(GetClientRect(hWindow, &rc)); + if(rc.right - rc.left < minWidth * pixelRatio) { + rc.right = rc.left + (LONG)(minWidth * pixelRatio); + } + if(rc.bottom - rc.top < minHeight * pixelRatio) { + rc.bottom = rc.top + (LONG)(minHeight * pixelRatio); + } + } + + void FreezePosition(SettingsRef settings, const std::string &key) override { + sscheck(GetWindowPlacement(hWindow, &placement)); + + BOOL isMaximized; + sscheck(isMaximized = IsZoomed(hWindow)); + + RECT rc = placement.rcNormalPosition; + settings->FreezeInt(key + "_Left", rc.left); + settings->FreezeInt(key + "_Right", rc.right); + settings->FreezeInt(key + "_Top", rc.top); + settings->FreezeInt(key + "_Bottom", rc.bottom); + settings->FreezeBool(key + "_Maximized", isMaximized == TRUE); + } + + void ThawPosition(SettingsRef settings, const std::string &key) override { + sscheck(GetWindowPlacement(hWindow, &placement)); + + RECT rc = placement.rcNormalPosition; + rc.left = settings->ThawInt(key + "_Left", rc.left); + rc.right = settings->ThawInt(key + "_Right", rc.right); + rc.top = settings->ThawInt(key + "_Top", rc.top); + rc.bottom = settings->ThawInt(key + "_Bottom", rc.bottom); + + MONITORINFO mi; + mi.cbSize = sizeof(mi); + sscheck(GetMonitorInfo(MonitorFromRect(&rc, MONITOR_DEFAULTTONEAREST), &mi)); + + // If it somehow ended up off-screen, then put it back. + // and make it visible by at least this portion of the screen + const LONG movein = 40; + + RECT mrc = mi.rcMonitor; + rc.left = Clamp(rc.left, mrc.left, mrc.right, 0, movein); + rc.right = Clamp(rc.right, mrc.left, mrc.right, movein, 0); + rc.top = Clamp(rc.top, mrc.top, mrc.bottom, 0, movein); + rc.bottom = Clamp(rc.bottom, mrc.top, mrc.bottom, movein, 0); + + // And make sure the minimum size is respected. (We can freeze a size smaller + // than minimum size if the DPI changed between runs.) + sscheck(SendMessageW(hWindow, WM_SIZING, WMSZ_BOTTOMRIGHT, (LPARAM)&rc)); + + placement.flags = 0; + if(settings->ThawBool(key + "_Maximized", false)) { + placement.showCmd = SW_SHOWMAXIMIZED; + } else { + placement.showCmd = SW_SHOW; + } + placement.rcNormalPosition = rc; + sscheck(SetWindowPlacement(hWindow, &placement)); + } + + void SetCursor(Cursor cursor) override { + LPWSTR cursorName = IDC_ARROW; + switch(cursor) { + case Cursor::POINTER: cursorName = IDC_ARROW; break; + case Cursor::HAND: cursorName = IDC_HAND; break; + } + + HCURSOR hCursor; + sscheck(hCursor = LoadCursorW(NULL, cursorName)); + sscheck(::SetCursor(hCursor)); + } + + void SetTooltip(const std::string &newText, double x, double y, + double width, double height) override { + if(newText == tooltipText) return; + tooltipText = newText; + + if(!newText.empty()) { + double pixelRatio = GetDevicePixelRatio(); + RECT toolRect; + toolRect.left = (int)(x * pixelRatio); + toolRect.top = (int)(y * pixelRatio); + toolRect.right = toolRect.left + (int)(width * pixelRatio); + toolRect.bottom = toolRect.top + (int)(height * pixelRatio); + + std::wstring newTextW = Widen(newText); + TOOLINFOW ti = {}; + ti.cbSize = sizeof(ti); + ti.hwnd = hWindow; + ti.rect = toolRect; + ti.lpszText = &newTextW[0]; + sscheck(SendMessageW(hTooltip, TTM_UPDATETIPTEXTW, 0, (LPARAM)&ti)); + sscheck(SendMessageW(hTooltip, TTM_NEWTOOLRECTW, 0, (LPARAM)&ti)); + } + // The following SendMessage call sometimes fails with ERROR_ACCESS_DENIED for + // no discernible reason, but only on wine. + SendMessageW(hTooltip, TTM_ACTIVATE, !newText.empty(), 0); + } + + bool IsEditorVisible() override { + BOOL visible; + visible = IsWindowVisible(hEditor); + return visible == TRUE; + } + + void ShowEditor(double x, double y, double fontHeight, double minWidth, + bool isMonospace, const std::string &text) override { + if(IsEditorVisible()) return; + + double pixelRatio = GetDevicePixelRatio(); + + HFONT hFont = CreateFontW(-(int)(fontHeight * GetDevicePixelRatio()), 0, 0, 0, + FW_REGULAR, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, + DEFAULT_QUALITY, FF_DONTCARE, isMonospace ? L"Lucida Console" : L"Arial"); + if(hFont == NULL) { + sscheck(hFont = (HFONT)GetStockObject(SYSTEM_FONT)); + } + sscheck(SendMessageW(hEditor, WM_SETFONT, (WPARAM)hFont, FALSE)); + sscheck(SendMessageW(hEditor, EM_SETMARGINS, EC_LEFTMARGIN|EC_RIGHTMARGIN, 0)); + + std::wstring textW = Widen(text); + + HDC hDc; + TEXTMETRICW tm; + SIZE ts; + sscheck(hDc = GetDC(hEditor)); + sscheck(SelectObject(hDc, hFont)); + sscheck(GetTextMetricsW(hDc, &tm)); + sscheck(GetTextExtentPoint32W(hDc, textW.c_str(), textW.length(), &ts)); + sscheck(ReleaseDC(hEditor, hDc)); + + RECT rc; + rc.left = (LONG)(x * pixelRatio); + rc.top = (LONG)(y * pixelRatio) - tm.tmAscent; + // Add one extra char width to avoid scrolling. + rc.right = (LONG)(x * pixelRatio) + + std::max((LONG)(minWidth * pixelRatio), ts.cx + tm.tmAveCharWidth); + rc.bottom = (LONG)(y * pixelRatio) + tm.tmDescent; + sscheck(ssAdjustWindowRectExForDpi(&rc, 0, /*bMenu=*/FALSE, WS_EX_CLIENTEDGE, + ssGetDpiForWindow(hWindow))); + + sscheck(MoveWindow(hEditor, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, + /*bRepaint=*/true)); + ShowWindow(hEditor, SW_SHOW); + if(!textW.empty()) { + sscheck(SendMessageW(hEditor, WM_SETTEXT, 0, (LPARAM)textW.c_str())); + sscheck(SendMessageW(hEditor, EM_SETSEL, 0, textW.length())); + sscheck(SetFocus(hEditor)); + } + } + + void HideEditor() override { + if(!IsEditorVisible()) return; + + ShowWindow(hEditor, SW_HIDE); + } + + void SetScrollbarVisible(bool visible) override { + if(scrollbarVisible != visible) { + scrollbarVisible = visible; + sscheck(ShowScrollBar(hWindow, SB_VERT, visible)); + // Force the window 1 pixel taller and then restore it's original size. + // This somehow fixes the problem where on Windows 10 and 11 toggling + // the scroll bar would cause the client area of the text window to + // be stretched or compressed horizontally. It looks bad and the + // buttons on the toolbar do not line up with the hit areas. + // See: https://github.com/solvespace/solvespace/issues/681 + // This fix is the result of a long discussion with claude-sonnet-4.6 + // https://github.com/solvespace/solvespace/agents/pull/1680 + // https://github.com/solvespace/solvespace/pull/1680 + RECT rc; + GetWindowRect(hWindow, &rc); + SetWindowPos(hWindow, HWND_TOP, rc.left, rc.top, rc.right - rc.left, + rc.bottom - rc.top + 1, SWP_FRAMECHANGED); + SetWindowPos(hWindow, HWND_TOP, rc.left, rc.top, rc.right - rc.left, + rc.bottom - rc.top, SWP_FRAMECHANGED); + } + } + + void ConfigureScrollbar(double min, double max, double pageSize) override { + SCROLLINFO si = {}; + si.cbSize = sizeof(si); + si.fMask = SIF_RANGE|SIF_PAGE; + si.nMin = (UINT)(min * SCROLLBAR_UNIT); + si.nMax = (UINT)(max * SCROLLBAR_UNIT); + si.nPage = (UINT)(pageSize * SCROLLBAR_UNIT); + SetScrollInfo(hWindow, SB_VERT, &si, /*redraw=*/TRUE); // Returns scrollbar position + } + + double GetScrollbarPosition() override { + if(!scrollbarVisible) return 0.0; + + SCROLLINFO si = {}; + si.cbSize = sizeof(si); + si.fMask = SIF_POS; + sscheck(GetScrollInfo(hWindow, SB_VERT, &si)); + return (double)si.nPos / SCROLLBAR_UNIT; + } + + void SetScrollbarPosition(double pos) override { + if(!scrollbarVisible) return; + + SCROLLINFO si = {}; + si.cbSize = sizeof(si); + si.fMask = SIF_POS; + sscheck(GetScrollInfo(hWindow, SB_VERT, &si)); + if(si.nPos == (int)(pos * SCROLLBAR_UNIT)) + return; + + si.nPos = (int)(pos * SCROLLBAR_UNIT); + SetScrollInfo(hWindow, SB_VERT, &si, /*redraw=*/TRUE); // Returns scrollbar position + + // Windows won't synthesize a WM_VSCROLL for us here. + if(onScrollbarAdjusted) { + onScrollbarAdjusted((double)si.nPos / SCROLLBAR_UNIT); + } + } + + void Invalidate() override { + sscheck(InvalidateRect(hWindow, NULL, /*bErase=*/FALSE)); + } +}; + +#if HAVE_OPENGL == 3 +EGLDisplay WindowImplWin32::eglDisplay = EGL_NO_DISPLAY; +#endif + +WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { + return std::make_shared(kind, + std::static_pointer_cast(parentWindow)); +} + +//----------------------------------------------------------------------------- +// 3DConnexion support +//----------------------------------------------------------------------------- + +#if defined(HAVE_SPACEWARE) +static HWND hSpaceWareDriverClass; + +void Open3DConnexion() { + hSpaceWareDriverClass = FindWindowW(L"SpaceWare Driver Class", NULL); + if(hSpaceWareDriverClass != NULL) { + SiInitialize(); + } +} + +void Close3DConnexion() { + if(hSpaceWareDriverClass != NULL) { + SiTerminate(); + } +} + +void Request3DConnexionEventsForWindow(WindowRef window) { + std::shared_ptr windowImpl = + std::static_pointer_cast(window); + if(hSpaceWareDriverClass != NULL) { + SiOpenWinInit(&windowImpl->sod, windowImpl->hWindow); + windowImpl->hSpaceWare = SiOpen("SolveSpace", SI_ANY_DEVICE, SI_NO_MASK, SI_EVENT, + &windowImpl->sod); + SiSetUiMode(windowImpl->hSpaceWare, SI_UI_NO_CONTROLS); + } +} +#else +void Open3DConnexion() {} +void Close3DConnexion() {} +void Request3DConnexionEventsForWindow(WindowRef window) {} +#endif + +//----------------------------------------------------------------------------- +// Message dialogs +//----------------------------------------------------------------------------- + +class MessageDialogImplWin32 final : public MessageDialog { +public: + MSGBOXPARAMSW mbp = {}; + + int style; + + std::wstring titleW; + std::wstring messageW; + std::wstring descriptionW; + std::wstring textW; + + std::vector buttons; + int defaultButton; + + MessageDialogImplWin32() { + mbp.cbSize = sizeof(mbp); + SetTitle("Message"); + } + + void SetType(Type type) override { + switch(type) { + case Type::INFORMATION: + style = MB_USERICON; // Avoid beep + mbp.hInstance = GetModuleHandle(NULL); + mbp.lpszIcon = MAKEINTRESOURCE(4000); // Use SolveSpace icon + // mbp.lpszIcon = IDI_INFORMATION; + break; + + case Type::QUESTION: + style = MB_ICONQUESTION; + break; + + case Type::WARNING: + style = MB_ICONWARNING; + break; + + case Type::ERROR: + style = MB_USERICON; // Avoid beep + mbp.hInstance = GetModuleHandle(NULL); + mbp.lpszIcon = MAKEINTRESOURCE(4000); // Use SolveSpace icon + // mbp.lpszIcon = IDI_ERROR; + break; + } + } + + void SetTitle(std::string title) override { + titleW = PrepareTitle(title); + mbp.lpszCaption = titleW.c_str(); + } + + void SetMessage(std::string message) override { + messageW = Widen(message); + UpdateText(); + } + + void SetDescription(std::string description) override { + descriptionW = Widen(description); + UpdateText(); + } + + void UpdateText() { + textW = messageW + L"\n\n" + descriptionW; + mbp.lpszText = textW.c_str(); + } + + void AddButton(std::string _label, Response response, bool isDefault) override { + int button; + switch(response) { + default: + case Response::NONE: ssassert(false, "Invalid response"); + case Response::OK: button = IDOK; break; + case Response::YES: button = IDYES; break; + case Response::NO: button = IDNO; break; + case Response::CANCEL: button = IDCANCEL; break; + } + buttons.push_back(button); + if(isDefault) { + defaultButton = button; + } + } + + Response RunModal() override { + mbp.dwStyle = style; + + std::sort(buttons.begin(), buttons.end()); + if(buttons == std::vector({ IDOK })) { + mbp.dwStyle |= MB_OK; + } else if(buttons == std::vector({ IDOK, IDCANCEL })) { + mbp.dwStyle |= MB_OKCANCEL; + } else if(buttons == std::vector({ IDYES, IDNO })) { + mbp.dwStyle |= MB_YESNO; + } else if(buttons == std::vector({ IDCANCEL, IDYES, IDNO })) { + mbp.dwStyle |= MB_YESNOCANCEL; + } else { + ssassert(false, "Unexpected button set"); + } + + switch(MessageBoxIndirectW(&mbp)) { + case IDOK: return Response::OK; break; + case IDYES: return Response::YES; break; + case IDNO: return Response::NO; break; + case IDCANCEL: return Response::CANCEL; break; + default: ssassert(false, "Unexpected response"); + } + } +}; + +MessageDialogRef CreateMessageDialog(WindowRef parentWindow) { + std::shared_ptr dialog = std::make_shared(); + dialog->mbp.hwndOwner = std::static_pointer_cast(parentWindow)->hWindow; + return dialog; +} + +//----------------------------------------------------------------------------- +// File dialogs +//----------------------------------------------------------------------------- + +class FileDialogImplWin32 final : public FileDialog { +public: + OPENFILENAMEW ofn = {}; + bool isSaveDialog; + std::wstring titleW; + std::wstring filtersW; + std::wstring defExtW; + std::wstring initialDirW; + // UNC paths may be as long as 32767 characters. + // Unfortunately, the Get*FileName API does not provide any way to use it + // except with a preallocated buffer of fixed size, so we use something + // reasonably large. + wchar_t filenameWC[32768] = {}; + + FileDialogImplWin32() { + ofn.lStructSize = sizeof(ofn); + ofn.lpstrFile = filenameWC; + ofn.nMaxFile = sizeof(filenameWC) / sizeof(wchar_t); + ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | + OFN_OVERWRITEPROMPT; + } + + void SetTitle(std::string title) override { + titleW = PrepareTitle(title); + ofn.lpstrTitle = titleW.c_str(); + } + + void SetCurrentName(std::string name) override { + SetFilename(GetFilename().Parent().Join(name)); + } + + Platform::Path GetFilename() override { + return Path::From(Narrow(filenameWC)); + } + + void SetFilename(Platform::Path path) override { + wcsncpy(filenameWC, Widen(path.raw).c_str(), sizeof(filenameWC) / sizeof(wchar_t) - 1); + } + + void SuggestFilename(Platform::Path path) override { + SetFilename(Platform::Path::From(path.FileStem())); + } + + void AddFilter(std::string name, std::vector extensions) override { + std::string desc, patterns; + for(auto &extension : extensions) { + std::string pattern = "*." + extension; + if(!desc.empty()) desc += ", "; + desc += pattern; + if(!patterns.empty()) patterns += ";"; + patterns += pattern; + } + filtersW += Widen(name + " (" + desc + ")" + '\0' + patterns + '\0'); + ofn.lpstrFilter = filtersW.c_str(); + if(ofn.lpstrDefExt == NULL) { + defExtW = Widen(extensions.front()); + ofn.lpstrDefExt = defExtW.c_str(); + } + } + + void FreezeChoices(SettingsRef settings, const std::string &key) override { + settings->FreezeString("Dialog_" + key + "_Folder", GetFilename().Parent().raw); + settings->FreezeInt("Dialog_" + key + "_Filter", ofn.nFilterIndex); + } + + void ThawChoices(SettingsRef settings, const std::string &key) override { + initialDirW = Widen(settings->ThawString("Dialog_" + key + "_Folder", "")); + ofn.lpstrInitialDir = initialDirW.c_str(); + ofn.nFilterIndex = settings->ThawInt("Dialog_" + key + "_Filter", 0); + } + + bool RunModal() override { + if(isSaveDialog) { + SetTitle(C_("title", "Save File")); + if(GetFilename().IsEmpty()) { + SetFilename(Path::From(_("untitled"))); + } + return GetSaveFileNameW(&ofn) == TRUE; + } else { + SetTitle(C_("title", "Open File")); + return GetOpenFileNameW(&ofn) == TRUE; + } + } +}; + +FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) { + std::shared_ptr dialog = std::make_shared(); + dialog->ofn.hwndOwner = std::static_pointer_cast(parentWindow)->hWindow; + dialog->isSaveDialog = false; + return dialog; +} + +FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) { + std::shared_ptr dialog = std::make_shared(); + dialog->ofn.hwndOwner = std::static_pointer_cast(parentWindow)->hWindow; + dialog->isSaveDialog = true; + return dialog; +} + +//----------------------------------------------------------------------------- +// Application-wide APIs +//----------------------------------------------------------------------------- + +std::vector GetFontFiles() { + std::vector fonts; + + std::wstring fontsDirW(MAX_PATH, '\0'); + fontsDirW.resize(GetWindowsDirectoryW(&fontsDirW[0], fontsDirW.length())); + fontsDirW += L"\\fonts\\"; + Platform::Path fontsDir = Platform::Path::From(Narrow(fontsDirW)); + + WIN32_FIND_DATAW wfd; + HANDLE h = FindFirstFileW((fontsDirW + L"*").c_str(), &wfd); + while(h != INVALID_HANDLE_VALUE) { + fonts.push_back(fontsDir.Join(Narrow(wfd.cFileName))); + if(!FindNextFileW(h, &wfd)) break; + } + + return fonts; +} + +void OpenInBrowser(const std::string &url) { + ShellExecuteW(NULL, L"open", Widen(url).c_str(), NULL, NULL, SW_SHOWNORMAL); +} + +std::vector InitGui(int argc, char **argv) { + std::vector args = InitCli(argc, argv); + + INITCOMMONCONTROLSEX icc; + icc.dwSize = sizeof(icc); + icc.dwICC = ICC_STANDARD_CLASSES|ICC_BAR_CLASSES; + InitCommonControlsEx(&icc); + + if(!SetLocale((uint16_t)GetUserDefaultLCID())) { + SetLocale("en_US"); + } + + return args; +} + +void RunGui() { + MSG msg; + // The return value of the following functions doesn't indicate success or failure. + while(GetMessage(&msg, NULL, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } +} + +void ExitGui() { + PostQuitMessage(0); +} + +void ClearGui() {} + +} +} diff --git a/src/platform/html/emshell.html b/src/platform/html/emshell.html new file mode 100644 index 0000000..951d588 --- /dev/null +++ b/src/platform/html/emshell.html @@ -0,0 +1,91 @@ + +SolveSpace Web Edition (EXPERIMENTAL)
+
+
+
Downloading...
+ + +
+
{{{ SCRIPT }}} diff --git a/src/platform/html/filemanagerui.js b/src/platform/html/filemanagerui.js new file mode 100644 index 0000000..c48e1a5 --- /dev/null +++ b/src/platform/html/filemanagerui.js @@ -0,0 +1,525 @@ +"use strict"; + +const FileManagerUI_OPEN = 0; +const FileManagerUI_SAVE = FileManagerUI_OPEN + 1; +const FileManagerUI_BROWSE = FileManagerUI_SAVE + 1; + +//FIXME(emscripten): File size thresholds. How large file can we accept safely ? + +/** Maximum filesize for a uploaded file. + * @type {number} */ +const FileManagerUI_UPLOAD_FILE_SIZE_LIMIT = 50 * 1000 * 1000; + +const tryMakeDirectory = (path) => { + try { + FS.mkdir(path); + } catch { + // NOP + } +} + + +class FileManagerUI { + /** + * @param {number} mode - dialog mode FileManagerUI_[ OPEN, SAVE, BROWSE ] + */ + constructor(mode) { + /** @type {boolean} */ + this.__isOpenDialog = false; + /** @type {boolean} */ + this.__isSaveDialog = false; + /** @type {boolean} */ + this.__isBrowseDialog = false; + + if (mode == FileManagerUI_OPEN) { + this.__isOpenDialog = true; + } else if (mode == FileManagerUI_SAVE) { + this.__isSaveDialog = true; + } else { + this.__isBrowseDialog = true; + } + + /** @type {boolean} true if the dialog is shown. */ + this.__isShown = false; + + /** @type {string[]} */ + this.__extension_filters = [".slvs"]; + + /** @type {string} */ + this.__basePathInFilesystem = ""; + + /** @type {string} filename user selected. empty if nothing selected */ + this.__selectedFilename = ""; + + this.__closedWithCancel = false; + + this.__defaultFilename = "untitled"; + } + + /** deconstructor + */ + dispose() { + if (this.__dialogRootElement) { + this.__dialogHeaderElement = null; + this.__descriptionElement = null; + this.__filelistElement = null; + this.__fileInputElement = null; + this.__saveFilenameInputElement = null; + this.__buttonContainerElement = null; + this.__dialogRootElement.parentElement.removeChild(this.__dialogRootElement); + this.__dialogRootElement = null; + } + } + + /** + * @param {string} label + * @param {string} response + * @param {bool} isDefault + */ + __addButton(label, response, isDefault, onclick) { + const buttonElem = document.createElement("div"); + addClass(buttonElem, "button"); + setLabelWithMnemonic(buttonElem, label); + if (isDefault) { + addClass(buttonElem, "default"); + addClass(buttonElem, "selected"); + } + buttonElem.addEventListener("click", () => { + if (onclick) { + if (onclick()) { + this.__close(); + } + } else { + this.__close(); + } + }); + + this.__buttonContainerElement.appendChild(buttonElem); + } + + /** + * @param {HTMLElement} div element that built + */ + buildDialog() { + const root = document.createElement('div'); + addClass(root, "modal"); + root.style.display = "none"; + root.style.zIndex = 1000; + + const dialog = document.createElement('div'); + addClass(dialog, "dialog"); + addClass(dialog, "wide"); + root.appendChild(dialog); + + const messageHeader = document.createElement('strong'); + this.__dialogHeaderElement = messageHeader; + addClass(messageHeader, "dialog_header"); + dialog.appendChild(messageHeader); + + const description = document.createElement('p'); + this.__descriptionElement = description; + dialog.appendChild(description); + + const filelistheader = document.createElement('h3'); + filelistheader.textContent = 'Files:'; + dialog.appendChild(filelistheader); + + const filelist = document.createElement('ul'); + this.__filelistElement = filelist; + addClass(filelist, 'filelist'); + dialog.appendChild(filelist); + + const dummyfilelistitem = document.createElement('li'); + dummyfilelistitem.textContent = "(No file in pseudo filesystem)"; + filelist.appendChild(dummyfilelistitem); + + if (this.__isOpenDialog) { + const fileuploadcontainer = document.createElement('div'); + dialog.appendChild(fileuploadcontainer); + + const fileuploadheader = document.createElement('h3'); + fileuploadheader.textContent = "Upload file:"; + fileuploadcontainer.appendChild(fileuploadheader); + + const dragdropdescription = document.createElement('p'); + dragdropdescription.textContent = "(Drag & drop file to the following box)"; + dragdropdescription.style.fontSize = "0.8em"; + dragdropdescription.style.margin = "0.1em"; + fileuploadcontainer.appendChild(dragdropdescription); + + const filedroparea = document.createElement('div'); + addClass(filedroparea, 'filedrop'); + filedroparea.addEventListener('dragstart', (ev) => this.__onFileDragDrop(ev)); + filedroparea.addEventListener('dragover', (ev) => this.__onFileDragDrop(ev)); + filedroparea.addEventListener('dragleave', (ev) => this.__onFileDragDrop(ev)); + filedroparea.addEventListener('drop', (ev) => this.__onFileDragDrop(ev)); + fileuploadcontainer.appendChild(filedroparea); + + const fileinput = document.createElement('input'); + this.__fileInputElement = fileinput; + fileinput.setAttribute('type', 'file'); + fileinput.style.width = "100%"; + fileinput.addEventListener('change', (ev) => this.__onFileInputChanged(ev)); + filedroparea.appendChild(fileinput); + + } else if (this.__isSaveDialog) { + const filenameinputcontainer = document.createElement('div'); + dialog.appendChild(filenameinputcontainer); + + const filenameinputheader = document.createElement('h3'); + filenameinputheader.textContent = "Filename:"; + filenameinputcontainer.appendChild(filenameinputheader); + + const filenameinput = document.createElement('input'); + filenameinput.setAttribute('type', 'input'); + filenameinput.style.width = "90%"; + filenameinput.style.margin = "auto 1em auto 1em"; + this.__saveFilenameInputElement = filenameinput; + filenameinputcontainer.appendChild(filenameinput); + } + + // Paragraph element for spacer + dialog.appendChild(document.createElement('p')); + + const buttoncontainer = document.createElement('div'); + this.__buttonContainerElement = buttoncontainer; + addClass(buttoncontainer, "buttons"); + dialog.appendChild(buttoncontainer); + + this.__addButton('OK', 0, false, () => { + if (this.__isOpenDialog) { + let selectedFilename = null; + const fileitems = document.querySelectorAll('input[type="radio"][name="filemanager_filelist"]'); + Array.from(fileitems).forEach((radiobox) => { + if (radiobox.checked) { + selectedFilename = radiobox.parentElement.getAttribute('data-filename'); + } + }); + if (selectedFilename) { + return true; + } else { + return false; + } + } else { + return true; + } + }); + + this.__addButton('Cancel', 1, true, () => { + this.__closedWithCancel = true; + return true; + }); + + return root; + } + + /** + * @param {string} text + */ + setTitle(text) { + this.__dialogHeaderText = text; + } + + /** + * @param {string} text + */ + setDescription(text) { + this.__descriptionText = text; + } + + /** + * @param {string} path file prefix. (ex) 'tmp/' to '/tmp/filename.txt' + */ + setBasePath(path) { + this.__basePathInFilesystem = path; + tryMakeDirectory(path); + } + + /** + * @param {string} filename + */ + setDefaultFilename(filename) { + this.__defaultFilename = filename; + } + + /** + * + * @param {string} filter comma-separated extensions like ".slvs,.stl;." + */ + setFilter(filter) { + const exts = filter.split(','); + this.__extension_filters = exts; + } + + __buildFileEntry(filename) { + const lielem = document.createElement('li'); + const label = document.createElement('label'); + label.setAttribute('data-filename', filename); + lielem.appendChild(label); + const radiobox = document.createElement('input'); + radiobox.setAttribute('type', 'radio'); + if (!this.__isOpenDialog) { + radiobox.style.display = "none"; + } + radiobox.setAttribute('name', 'filemanager_filelist'); + label.appendChild(radiobox); + const filenametext = document.createTextNode(filename); + label.appendChild(filenametext); + + return lielem; + } + + /** + * @returns {string[]} filename array + */ + __getFileEntries() { + const basePath = this.__basePathInFilesystem; + /** @type {any[]} */ + const nodes = FS.readdir(basePath); + /** @type {string[]} */ + const files = nodes.filter((nodename) => { + return FS.isFile(FS.lstat(basePath + nodename).mode); + }); + /*.map((filename) => { + return basePath + filename; + });*/ + console.log(`__getFileEntries():`, files); + return files; + } + + /** + * @param {string[]?} files file list already constructed + * @returns {string[]} filename array + */ + __getFileEntries_recurse(basePath) { + //FIXME:remove try catch block + try { + //const basePath = this.__basePathInFilesystem; + FS.currentPath = basePath; + /** @type {any[]} */ + const nodes = FS.readdir(basePath); + + const filesInThisDirectory = nodes.filter((nodename) => { + return FS.isFile(FS.lstat(basePath + "/" + nodename).mode); + }).map((filename) => { + return basePath + "/" + filename; + }); + let files = filesInThisDirectory; + + const directories = nodes.filter((nodename) => { + return FS.isDir(FS.lstat(basePath + "/" + nodename).mode); + }); + + for (let i = 0; i < directories.length; i++) { + const directoryname = directories[i]; + if (directoryname == '.' || directoryname == '..') { + continue; + } + const orig_cwd = FS.currentPath; + const directoryfullpath = basePath + "/" + directoryname; + FS.currentPath = directoryfullpath; + files = files.concat(this.__getFileEntries_recurse(directoryfullpath)); + FS.currentPath = orig_cwd; + } + + console.log(`__getFileEntries_recurse(): in "${basePath}"`, files); + return files; + + } catch (excep) { + console.log(excep); + throw excep; + } + } + + __updateFileList() { + console.log(`__updateFileList()`); + Array.from(this.__filelistElement.children).forEach((elem) => { + this.__filelistElement.removeChild(elem); + }); + // const files = this.__getFileEntries(); + FS.currentPath = this.__basePathInFilesystem; + const files = this.__getFileEntries_recurse(this.__basePathInFilesystem); + if (files.length < 1) { + const dummyfilelistitem = document.createElement('li'); + dummyfilelistitem.textContent = "(No file in pseudo filesystem)"; + this.__filelistElement.appendChild(dummyfilelistitem); + + } else { + files.forEach((entry) => { + this.__filelistElement.appendChild(this.__buildFileEntry(entry)); + }); + } + } + + + /** + * @param {File} file + */ + __getFileAsArrayBuffer(file) { + return new Promise((resolve, reject) => { + const filereader = new FileReader(); + filereader.onerror = (ev) => { + reject(ev); + }; + filereader.onload = (ev) => { + resolve(ev.target.result); + }; + filereader.readAsArrayBuffer(file); + }); + } + + /** + * + * @param {File} file + */ + async __tryAddFile(file) { + return new Promise(async (resolve, reject) => { + if (!file) { + reject(new Error(`Invalid arg: file is ${file}`)); + + } else if (file.size > FileManagerUI_UPLOAD_FILE_SIZE_LIMIT) { + //FIXME(emscripten): Use our MessageDialog instead of browser's alert(). + alert(`Specified file is larger than limit of ${FileManagerUI_UPLOAD_FILE_SIZE_LIMIT} bytes. Canceced.`); + reject(new Error(`File is too large: "${file.name} is ${file.size} bytes`)); + + } else { + // Just add to Filesystem + const path = `${this.__basePathInFilesystem}${file.name}`; + const blobArrayBuffer = await this.__getFileAsArrayBuffer(file); + const u8array = new Uint8Array(blobArrayBuffer); + const fs = FS.open(path, "w"); + FS.write(fs, u8array, 0, u8array.length, 0); + FS.close(fs); + resolve(); + } + }); + } + + __addSelectedFile() { + if (this.__fileInputElement.files.length < 1) { + console.warn(`No file selected.`); + return; + } + + const file = this.__fileInputElement.files[0]; + this.__tryAddFile(file) + .then(() => { + this.__updateFileList(); + }) + .catch((err) => { + this.__fileInputElement.value = null; + console.error(err); + }) + } + + /** + * @param {DragEvent} ev + */ + __onFileDragDrop(ev) { + ev.preventDefault(); + if (ev.type == "dragenter" || ev.type == "dragover" || ev.type == "dragleave") { + return; + } + if (ev.dataTransfer.files.length < 1) { + return; + } + this.__fileInputElement.files = ev.dataTransfer.files; + + this.__addSelectedFile(); + } + + /** + * @param {InputEvent} _ev + */ + __onFileInputChanged(_ev) { + this.__addSelectedFile(); + } + + + /** Show the FileManager UI dialog */ + __show() { + this.__closedWithCancel = false; + + /** @type {HTMLElement} */ + this.__dialogRootElement = this.buildDialog(); + document.querySelector('body').appendChild(this.__dialogRootElement); + + this.__dialogHeaderElement.textContent = this.__dialogHeaderText || "File manager"; + this.__descriptionElement.textContent = this.__descriptionText || "Select a file."; + if (this.__extension_filters) { + this.__descriptionElement.textContent += "Requested filter is " + this.__extension_filters.join(", "); + } + + if (this.__isOpenDialog && this.__extension_filters) { + this.__fileInputElement.accept = this.__extension_filters.concat(','); + } + + if (this.__isSaveDialog) { + this.__saveFilenameInputElement.value = this.__defaultFilename; + } + + this.__dialogRootElement.style.display = "block"; + this.__isShown = true; + } + + /** Close the dialog */ + __close() { + this.__selectedFilename = ""; + if (this.__isOpenDialog) { + Array.from(document.querySelectorAll('input[type="radio"][name="filemanager_filelist"]')) + .forEach((elem) => { + if (elem.checked) { + this.__selectedFilename = elem.parentElement.getAttribute("data-filename"); + } + }); + } else if (this.__isSaveDialog) { + if (!this.__closedWithCancel) { + this.__selectedFilename = this.__saveFilenameInputElement.value; + } + } + + Array.from(this.__filelistElement.children).forEach((elem) => { + this.__filelistElement.removeChild(elem); + }); + + this.dispose(); + + this.__isShown = false; + } + + /** + * @return {boolean} + */ + isShown() { + return this.__isShown; + } + + /** + * + * @returns {Promise} filename string on resolved. + */ + showModalAsync() { + return new Promise((resolve, reject) => { + this.__show(); + this.__updateFileList(); + const intervalTimer = setInterval(() => { + if (!this.isShown()) { + clearInterval(intervalTimer); + resolve(this.__selectedFilename); + } + }, 50); + }); + } + + getSelectedFilename() { + return this.__selectedFilename; + } + + show() { + this.__show(); + this.__updateFileList(); + } +}; + + +window.FileManagerUI = FileManagerUI; diff --git a/src/platform/html/solvespaceui.css b/src/platform/html/solvespaceui.css new file mode 100644 index 0000000..3d2f44b --- /dev/null +++ b/src/platform/html/solvespaceui.css @@ -0,0 +1,344 @@ +* { + font-family: sans; +} +html, body { + padding: 0; + margin: 0; + background: black; + display: flex; + flex-direction: column; + height: 100%; +} +html, body, canvas, #splash, #container { + margin: 0; + padding: 0; + width: 100%; + height: 100%; +} +body { + overflow: hidden; +} + +/* Splashscreen */ +#splash { + z-index: 1000; + background: black; + color: white; + position: absolute; +} +#splash .center { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; +} +#splash a { + color: white; +} + +#spinner { + height: 30px; + width: 30px; + margin: 0px auto; + border-left: 10px solid rgb(255, 255, 255); + border-top: 10px solid rgb(0, 255, 0); + border-right: 10px solid rgb(255, 0, 255); + border-bottom: 10px solid rgb(0, 255, 0); + border-radius: 100%; + animation: rotation 3s linear infinite; + margin-bottom: 5px; +} +@keyframes rotation { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* Grid layout for main */ +main { + height: 100%; + + /* Use CSS Grid layout for vertical placement. */ + display: grid; + /* Row 0 for menubar (fit to content), Row 1 for canvas0, canvas1 (rest of space) */ + grid-template-rows: auto 1fr; +} + +/* Buttons */ +.button { + border: 1px solid hsl(0, 0%, 60%); + background: hsl(0, 0%, 10%); + color: white; + padding: 4px 8px; + cursor: default; +} +.button.selected { + background: hsl(0, 0%, 20%); +} +.button:hover { + background: hsl(0, 0%, 40%); +} + +/* Editors */ +.editor { + position: fixed; + padding: 0; + border: none; +} + +/* Menus */ +.menu { + font-size: 0; + margin: 0; + padding: 0; + padding-right: 10px; + list-style-type: none; + background: hsl(0, 0%, 20%); + color: white; + cursor: default; + + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* Normal menu items */ +.menu > li { + z-index: 100; + font-size: 16px; + display: inline-flex; + justify-content: space-between; + align-items: center; + white-space: nowrap; + position: relative; + width: 100%; + height: 19px; + margin: 2px; + padding: 3px; +} +.menu > li::before, .menu > li::after { + font-family: 'Font Awesome 5 Free'; + font-weight: 900; + font-size: 12px; +} +.menu > li.hover, +.menu > li.selected, +.menu.menubar > li:hover:not(.selected) { + background: hsl(0, 0%, 30%); +} +.menu > li.disabled { + color: hsl(0, 0%, 30%); +} + +/* Check and radio menu items */ +.menu > li { + padding-left: 24px; +} +.menu > li::before { + position: absolute; + text-align: center; + left: 0px; + width: 24px; +} +.menu > li.check::before { + content: '\f0c8'; +} +.menu > li.check.active::before { + content: '\f14a'; +} +.menu > li.radio::before { + content: '\f111'; +} +.menu > li.radio.active::before { + content: '\f192'; +} + +/* Separator menu items */ +.menu > li.separator { + height: 0px; + border-top: 1px solid hsl(0, 0%, 30%); + margin: 0 2px 0 2px; + padding-top: 0; + padding-bottom: 0; +} + +/* Accelerators */ +.menu > li > .accel { + text-align: right; + margin-left: 20px; +} + +/* Submenus */ +.menu > li > .menu, +.menu.popup { + display: none; + white-space: normal; + padding-right: 31px; +} +.menu > li.has-submenu::after { + content: '\f0da'; +} +.menu > li.selected > .menu, +.menu > li.hover > .menu, +.menu.popup { + display: block; + background: hsl(0, 0%, 10%); + border: 1px solid hsl(0, 0%, 30%); + position: absolute; + left: 100%; + top: -3px; +} + +/* Popup menus */ +.menu.popup { + display: block; + position: absolute; + width: min-content; +} + +/* Menubars */ +.menubar { + padding-left: 5px; +} +.menubar > li { + width: auto; + width: fit-content; + margin: 0; + padding: 5px; +} +.menubar > li.selected { + background: hsl(0, 0%, 10%); + border: 1px solid hsl(0, 0%, 30%); + padding: 4px; +} +.menubar.menu > li.selected > .menu { + display: block; + position: absolute; + left: -1px; + top: 27px; +} + +/* Modal popups */ +.modal { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: hsla(0, 0%, 0%, 60%); +} +.modal > div { + position: absolute; + top: 15%; + left: 50%; + transform: translate(-50%, 0%); +} + +/* Dialogs */ +.dialog { + border: 1px solid hsl(0, 0%, 30%); + background: hsl(0, 0%, 10%); + color: white; + padding: 20px; + display: flex; + flex-direction: column; + min-width: 200px; + max-width: 400px; + white-space: pre-wrap; + max-height: 70%; + overflow-y: auto; +} +.dialog.wide { + width: 80%; + max-width: 1200px; +} +.dialog > .buttons { + display: flex; + justify-content: space-around; +} +.dialog .filedrop { + margin: 1em 0 1em 0; + padding: 1em; + border: 2px solid black; + background-color: hsl(0, 0%, 50%); +} +.dialog .filelist { + display: flex; + flex-flow: row wrap; + list-style: none; + margin: 0; + padding: 0; +} +.dialog .filelist li { + padding: 0.2em 0.5em 0.2em 0.5em; + break-inside: avoid; +} + +/* Mnemonics */ +.label > u { + position: relative; + top: 0px; + text-decoration: none; +} +body.mnemonic .label > u { + border-bottom: 1px solid; +} + +/* Canvases */ +canvas { + border: 0px none; + background-color: black; +} + +#container { + display: flex; + overflow: hidden; +} +/* FIXME(emscripten): this should be dynamically adjustable, not hardcoded in CSS */ +#container0 { + flex-basis: 80%; + height: 100%; + position: relative; + overflow: hidden; +} + +#container1parent { + flex-basis: 20%; + height: 100%; + position: relative; + overflow: hidden; + min-width: 410px; + + display: grid; + grid-template-columns: auto 19px; + grid-template-rows: 100%; +} + +#container1 { + height: 100%; +} + +#canvas1scrollbarbox { + /* 19px is a magic number for scrollbar width (Yes, this is platform-dependent value but looks almost working.) */ + width: 19px; + min-width: 19px; + height: 100%; + overflow-x: hidden; + overflow-y: scroll; + + background-color: lightgray; + -webkit-overflow-scrolling: auto; +} + +#canvas1scrollbar { + /* 0px will disable the scrollbar by browser. */ + width: 1px; + /* Disable scrollbar as default. This value will be overwritten by program. */ + height: 100%; +} + +#view_separator { + width: 4px; + background: hsl(0, 0%, 20%); +} diff --git a/src/platform/html/solvespaceui.js b/src/platform/html/solvespaceui.js new file mode 100644 index 0000000..80af245 --- /dev/null +++ b/src/platform/html/solvespaceui.js @@ -0,0 +1,813 @@ +function isModal() { + var hasModal = !!document.querySelector('.modal'); + var hasMenuBar = !!document.querySelector('.menubar .selected'); + var hasPopupMenu = !!document.querySelector('.menu.popup'); + return hasModal || hasMenuBar || hasPopupMenu; +} + +/* String helpers */ + +/** + * @param {string} s - original string + * @param {number} digits - char length of generating string + * @param {string} ch - string to be used for padding + * @return {string} generated string ($digits chars length) or $s + */ +function stringPadLeft(s, digits, ch) { + if (s.length > digits) { + return s; + } + for (let i = s.length; i < digits; i++) { + s = ch + s; + } + return s; +} + +/** Generate a string expression of now + * @return {string} like a "2022_08_31_2245" string (for 2022-08-31 22:45; local time) + */ +function GetCurrentDateTimeString() { + const now = new Date(); + const padLeft2 = (num) => { return stringPadLeft(num.toString(), 2, '0') }; + return (`${now.getFullYear()}_${padLeft2(now.getMonth()+1)}_${padLeft2(now.getDate())}` + + `_` + `${padLeft2(now.getHours())}${padLeft2(now.getMinutes())}`); +} + +/* CSS helpers */ +function hasClass(element, className) { + return element.classList.contains(className); +} +function addClass(element, className) { + element.classList.add(className); +} +function removeClass(element, className) { + element.classList.remove(className); +} +function removeClassFromAllChildren(element, className) { + element.querySelectorAll('.' + className).forEach(function(element) { + removeClass(element, className); + }) +} + +/* Mnemonic helpers */ +function setLabelWithMnemonic(element, labelText) { + var label = document.createElement('span'); + addClass(label, 'label'); + element.appendChild(label); + + var matches = labelText.match('(.*?)&(.)(.*)?'); + if(matches) { + label.appendChild(document.createTextNode(matches[1])); + if(matches[2]) { + var mnemonic = document.createElement('u'); + mnemonic.innerText = matches[2]; + label.appendChild(mnemonic); + addClass(element, 'mnemonic-Key' + matches[2].toUpperCase()); + } + if(matches[3]) { + label.appendChild(document.createTextNode(matches[3])); + } + } else { + label.appendChild(document.createTextNode(labelText)) + } +} + +/** Touchevent helper + * @param {TouchEvent} event + * @return {boolean} true if same element is target of touchstart and touchend + */ +function isSameElementOnTouchstartAndTouchend(event) { + const elementOnTouchStart = event.target; + const elementOnTouchEnd = document.elementFromPoint(event.changedTouches[0].clientX, event.changedTouches[0].clientY); + return elementOnTouchStart == elementOnTouchEnd; +} + +/* Button helpers */ +function isButton(element) { + return hasClass(element, 'button'); +} + +/* Button DOM traversal helpers */ +function getButton(element) { + if(!element) return; + if(element.tagName == 'U') { + element = element.parentElement; + } + if(hasClass(element, 'label')) { + return getButton(element.parentElement); + } else if(isButton(element)) { + return element; + } +} + +/* Button behavior */ +window.addEventListener('click', function(event) { + var button = getButton(event.target); + if(button) { + button.dispatchEvent(new Event('trigger')); + } +}); +window.addEventListener("touchend", (event) => { + if (!isSameElementOnTouchstartAndTouchend(event)) { + return; + } + const button = getButton(event.target); + if (button) { + button.dispatchEvent(new Event('trigger')); + } +}); + +window.addEventListener('keydown', function(event) { + var selected = document.querySelector('.button.selected'); + if(!selected) return; + + var outSelected, newSelected; + if(event.key == 'ArrowRight') { + outSelected = selected; + newSelected = selected.nextElementSibling; + if(!newSelected) { + newSelected = outSelected.parentElement.firstElementChild; + } + } else if(event.key == 'ArrowLeft') { + outSelected = selected; + newSelected = selected.previousElementSibling; + if(!newSelected) { + newSelected = outSelected.parentElement.lastElementChild; + } + } else if(event.key == 'Enter') { + selected.dispatchEvent(new Event('trigger')); + } else if(event.key == 'Escape' && hasClass(selected, 'default')) { + selected.dispatchEvent(new Event('trigger')); + } + + if(outSelected) removeClass(outSelected, 'selected'); + if(newSelected) addClass(newSelected, 'selected'); + + event.stopPropagation(); +}); + +/* Editor helpers */ +function isEditor(element) { + return hasClass(element, 'editor'); +} + +/* Editor DOM traversal helpers */ +function getEditor(element) { + if(!element) return; + if(isEditor(element)) { + return element; + } +} + +/* Editor behavior */ +window.addEventListener('keydown', function(event) { + var editor = getEditor(event.target); + if(editor) { + if(event.key == 'Enter') { + editor.dispatchEvent(new Event('trigger')); + } else if(event.key == 'Escape') { + editor.style.display = 'none'; + } + event.stopPropagation(); + } +}, {capture: true}); + +/* Menu helpers */ +function isMenubar(element) { + return hasClass(element, 'menubar'); +} +function isMenu(element) { + return hasClass(element, 'menu'); +} +function isPopupMenu(element) { + return isMenu(element) && hasClass(element, 'popup') +} +function hasSubmenu(menuItem) { + return !!menuItem.querySelector('.menu'); +} + +/* Menu item helpers */ +function isMenuItemSelectable(menuItem) { + return !(hasClass(menuItem, 'disabled') || hasClass(menuItem, 'separator')); +} +function isMenuItemSelected(menuItem) { + return hasClass(menuItem, 'selected') || hasClass(menuItem, 'hover'); +} +function deselectMenuItem(menuItem) { + removeClass(menuItem, 'selected'); + removeClass(menuItem, 'hover'); + removeClassFromAllChildren(menuItem, 'selected'); + removeClassFromAllChildren(menuItem, 'hover'); +} +function selectMenuItem(menuItem) { + var menu = menuItem.parentElement; + removeClassFromAllChildren(menu, 'selected'); + removeClassFromAllChildren(menu, 'hover'); + if(isMenubar(menu)) { + addClass(menuItem, 'selected'); + } else { + addClass(menuItem, 'hover'); + } +} +function triggerMenuItem(menuItem) { + selectMenuItem(menuItem); + if(hasSubmenu(menuItem)) { + selectMenuItem(menuItem.querySelector('li:first-child')); + } else { + var parent = menuItem.parentElement; + while(!isMenubar(parent) && !isPopupMenu(parent)) { + parent = parent.parentElement; + } + removeClassFromAllChildren(parent, 'selected'); + removeClassFromAllChildren(parent, 'hover'); + if(isPopupMenu(parent)) { + parent.remove(); + } + + menuItem.dispatchEvent(new Event('trigger')); + } +} + +/* Menu DOM traversal helpers */ +function getMenuItem(element) { + if(!element) return; + if(element.tagName == 'U') { + element = element.parentElement; + } + if(hasClass(element, 'label')) { + return getMenuItem(element.parentElement); + } else if(element.tagName == 'LI' && isMenu(element.parentElement)) { + return element; + } +} +function getMenu(element) { + if(!element) return; + if(isMenu(element)) { + return element; + } else { + var menuItem = getMenuItem(element); + if(menuItem && isMenu(menuItem.parentElement)) { + return menuItem.parentElement; + } + } +} + +/* Menu behavior */ +window.addEventListener('click', function(event) { + var menuItem = getMenuItem(event.target); + var menu = getMenu(menuItem); + if(menu && isMenubar(menu)) { + if(hasClass(menuItem, 'selected')) { + removeClass(menuItem, 'selected'); + } else { + selectMenuItem(menuItem); + } + event.stopPropagation(); + } else if(menu) { + if(!hasSubmenu(menuItem)) { + triggerMenuItem(menuItem); + } + event.stopPropagation(); + } else { + document.querySelectorAll('.menu .selected, .menu .hover') + .forEach(function(menuItem) { + deselectMenuItem(menuItem); + event.stopPropagation(); + }); + document.querySelectorAll('.menu.popup') + .forEach(function(menu) { + menu.remove(); + }); + } +}); +window.addEventListener("touchend", (event) => { + if (!isSameElementOnTouchstartAndTouchend(event)) { + return; + } + var menuItem = getMenuItem(event.target); + var menu = getMenu(menuItem); + if(menu && isMenubar(menu)) { + if(hasClass(menuItem, 'selected')) { + removeClass(menuItem, 'selected'); + } else { + selectMenuItem(menuItem); + } + event.stopPropagation(); + event.preventDefault(); + } else if(menu) { + if(!hasSubmenu(menuItem)) { + triggerMenuItem(menuItem); + } else { + addClass(menuItem, "selected"); + addClass(menuItem, "hover"); + } + event.stopPropagation(); + } else { + document.querySelectorAll('.menu .selected, .menu .hover') + .forEach(function(menuItem) { + deselectMenuItem(menuItem); + event.stopPropagation(); + }); + document.querySelectorAll('.menu.popup') + .forEach(function(menu) { + menu.remove(); + }); + } +}); +window.addEventListener('mouseover', function(event) { + var menuItem = getMenuItem(event.target); + var menu = getMenu(menuItem); + if(menu) { + var selected = menu.querySelectorAll('.selected, .hover'); + if(isMenubar(menu)) { + if(selected.length > 0) { + selected.forEach(function(menuItem) { + if(selected != menuItem) { + deselectMenuItem(menuItem); + } + }); + addClass(menuItem, 'selected'); + } + } else { + if(isMenuItemSelectable(menuItem)) { + selectMenuItem(menuItem); + } + } + } +}); +window.addEventListener('keydown', function(event) { + var allSelected = document.querySelectorAll('.menubar .selected, .menubar .hover,' + + '.menu.popup .selected, .menu.popup .hover'); + if(allSelected.length == 0) return; + + var selected = allSelected[allSelected.length - 1]; + var outSelected, newSelected; + var isMenubarItem = isMenubar(getMenu(selected)); + + if(isMenubarItem && event.key == 'ArrowRight' || + !isMenubarItem && event.key == 'ArrowDown') { + outSelected = selected; + newSelected = selected.nextElementSibling; + while(newSelected && !isMenuItemSelectable(newSelected)) { + newSelected = newSelected.nextElementSibling; + } + if(!newSelected) { + newSelected = outSelected.parentElement.firstElementChild; + } + } else if(isMenubarItem && event.key == 'ArrowLeft' || + !isMenubarItem && event.key == 'ArrowUp') { + outSelected = selected; + newSelected = selected.previousElementSibling; + while(newSelected && !isMenuItemSelectable(newSelected)) { + newSelected = newSelected.previousElementSibling; + } + if(!newSelected) { + newSelected = outSelected.parentElement.lastElementChild; + } + } else if(!isMenubarItem && event.key == 'ArrowRight') { + if(hasSubmenu(selected)) { + selectMenuItem(selected.querySelector('li:first-child')); + } else { + outSelected = allSelected[0]; + newSelected = outSelected.nextElementSibling; + if(!newSelected) { + newSelected = outSelected.parentElement.firstElementChild; + } + } + } else if(!isMenubarItem && event.key == 'ArrowLeft') { + if(allSelected.length > 2) { + outSelected = selected; + } else { + outSelected = allSelected[0]; + newSelected = outSelected.previousElementSibling; + if(!newSelected) { + newSelected = outSelected.parentElement.lastElementChild; + } + } + } else if(isMenubarItem && event.key == 'ArrowDown') { + newSelected = selected.querySelector('li:first-child'); + } else if(event.key == 'Enter') { + triggerMenuItem(selected); + } else if(event.key == 'Escape') { + outSelected = allSelected[0]; + } else { + var withMnemonic = getMenu(selected).querySelector('.mnemonic-' + event.key); + if(withMnemonic) { + triggerMenuItem(withMnemonic); + } + } + + if(outSelected) deselectMenuItem(outSelected); + if(newSelected) selectMenuItem(newSelected); + + event.stopPropagation(); +}); + +/* Mnemonic behavior */ +window.addEventListener('keydown', function(event) { + var withMnemonic; + if(event.altKey && event.key == 'Alt') { + addClass(document.body, 'mnemonic'); + } else if(!isModal() && event.altKey && (withMnemonic = + document.querySelector('.menubar > .mnemonic-' + event.code))) { + triggerMenuItem(withMnemonic); + event.stopPropagation(); + } else { + removeClass(document.body, 'mnemonic'); + } +}); +window.addEventListener('keyup', function(event) { + if(event.key == 'Alt') { + removeClass(document.body, 'mnemonic'); + } +}); + + +// FIXME(emscripten): Should be implemented in guihtmlcpp ? +class FileUploadHelper { + constructor() { + this.modalRoot = document.createElement("div"); + addClass(this.modalRoot, "modal"); + this.modalRoot.style.display = "none"; + this.modalRoot.style.zIndex = 1000; + + this.dialogRoot = document.createElement("div"); + addClass(this.dialogRoot, "dialog"); + this.modalRoot.appendChild(this.dialogRoot); + + this.messageHeader = document.createElement("strong"); + this.dialogRoot.appendChild(this.messageHeader); + + this.descriptionParagraph = document.createElement("p"); + this.dialogRoot.appendChild(this.descriptionParagraph); + + this.currentFileListHeader = document.createElement("p"); + this.currentFileListHeader.textContent = "Current uploaded files:"; + this.dialogRoot.appendChild(this.currentFileListHeader); + + this.currentFileList = document.createElement("div"); + this.dialogRoot.appendChild(this.currentFileList); + + this.fileInputContainer = document.createElement("div"); + + this.fileInputElement = document.createElement("input"); + this.fileInputElement.setAttribute("type", "file"); + this.fileInputElement.addEventListener("change", (ev)=> this.onFileInputChanged(ev)); + this.fileInputContainer.appendChild(this.fileInputElement); + + this.dialogRoot.appendChild(this.fileInputContainer); + + this.buttonHolder = document.createElement("div"); + addClass(this.buttonHolder, "buttons"); + this.dialogRoot.appendChild(this.buttonHolder); + + this.AddButton("OK", 0, false); + this.AddButton("Cancel", 1, true); + + this.closeDialog(); + + document.querySelector("body").appendChild(this.modalRoot); + + this.currentFilename = null; + + // FIXME(emscripten): For debugging + this.title = ""; + this.filename = ""; + this.filters = ""; + } + + dispose() { + document.querySelector("body").removeChild(this.modalRoot); + } + + AddButton(label, response, isDefault) { + // FIXME(emscripten): implement + const buttonElem = document.createElement("div"); + addClass(buttonElem, "button"); + setLabelWithMnemonic(buttonElem, label); + if (isDefault) { + addClass(buttonElem, "default"); + addClass(buttonElem, "selected"); + } + buttonElem.addEventListener("click", () => { + this.closeDialog(); + }); + + this.buttonHolder.appendChild(buttonElem); + } + + getFileEntries() { + const basePath = '/'; + /** @type {Array { + return FS.isFile(FS.lstat(basePath + nodename).mode); + }).map((filename) => { + return basePath + filename; + }); + return files; + } + + generateFileList() { + let filepaths = this.getFileEntries(); + const listElem = document.createElement("ul"); + for (let i = 0; i < filepaths.length; i++) { + const listitemElem = document.createElement("li"); + const stat = FS.lstat(filepaths[i]); + const text = `"${filepaths[i]}" (${stat.size} bytes)`; + listitemElem.textContent = text; + listElem.appendChild(listitemElem); + } + return listElem; + } + + updateFileList() { + this.currentFileList.innerHTML = ""; + this.currentFileList.appendChild(this.generateFileList()); + } + + onFileInputChanged(ev) { + const selectedFiles = ev.target.files; + if (selectedFiles.length < 1) { + return; + } + const selectedFile = selectedFiles[0]; + const selectedFilename = selectedFile.name; + this.filename = selectedFilename; + this.currentFilename = selectedFilename; + + // Prepare FileReader + const fileReader = new FileReader(); + const fileReaderReadAsArrayBufferPromise = new Promise((resolve, reject) => { + fileReader.addEventListener("load", (ev) => { + resolve(ev.target.result); + }); + fileReader.addEventListener("abort", (err) => { + reject(err); + }); + fileReader.readAsArrayBuffer(selectedFile); + }); + + fileReaderReadAsArrayBufferPromise + .then((arrayBuffer) => { + // Write selected file to FS + console.log(`Write uploaded file blob to filesystem. "${selectedFilename}" (${arrayBuffer.byteLength} bytes)`); + const u8array = new Uint8Array(arrayBuffer); + const fs = FS.open("/" + selectedFilename, "w"); + FS.write(fs, u8array, 0, u8array.length, 0); + FS.close(fs); + + // Update file list in dialog + this.updateFileList(); + }) + .catch((err) => { + console.error("Error while fileReader.readAsArrayBuffer():", err); + }); + } + + showDialog() { + this.updateFileList(); + + this.is_shown = true; + this.modalRoot.style.display = "block"; + } + + closeDialog() { + this.is_shown = false; + this.modalRoot.style.display = "none"; + } +}; + +// FIXME(emscripten): Workaround +function createFileUploadHelperInstance() { + return new FileUploadHelper(); +} + +// FIXME(emscripten): Should be implemented in guihtmlcpp ? +class FileDownloadHelper { + constructor() { + this.modalRoot = document.createElement("div"); + addClass(this.modalRoot, "modal"); + this.modalRoot.style.display = "none"; + this.modalRoot.style.zIndex = 1000; + + this.dialogRoot = document.createElement("div"); + addClass(this.dialogRoot, "dialog"); + this.modalRoot.appendChild(this.dialogRoot); + + this.messageHeader = document.createElement("strong"); + this.dialogRoot.appendChild(this.messageHeader); + + this.descriptionParagraph = document.createElement("p"); + this.dialogRoot.appendChild(this.descriptionParagraph); + + this.buttonHolder = document.createElement("div"); + addClass(this.buttonHolder, "buttons"); + this.dialogRoot.appendChild(this.buttonHolder); + + this.closeDialog(); + + document.querySelector("body").appendChild(this.modalRoot); + } + + dispose() { + document.querySelector("body").removeChild(this.modalRoot); + } + + AddButton(label, response, isDefault) { + // FIXME(emscripten): implement + const buttonElem = document.createElement("div"); + addClass(buttonElem, "button"); + setLabelWithMnemonic(buttonElem, label); + if (isDefault) { + addClass(buttonElem, "default"); + addClass(buttonElem, "selected"); + } + buttonElem.addEventListener("click", () => { + this.closeDialog(); + this.dispose(); + }); + + this.buttonHolder.appendChild(buttonElem); + } + + createBlobURLFromArrayBuffer(arrayBuffer) { + const u8array = new Uint8Array(arrayBuffer); + let dataUrl = "data:application/octet-stream;base64,"; + let binaryString = ""; + for (let i = 0; i < u8array.length; i++) { + binaryString += String.fromCharCode(u8array[i]); + } + dataUrl += btoa(binaryString); + + return dataUrl; + } + + prepareFile(filename) { + this.messageHeader.textContent = "Your file ready"; + + const stat = FS.lstat(filename); + const filesize = stat.size; + const fs = FS.open(filename, "r"); + const readbuffer = new Uint8Array(filesize); + FS.read(fs, readbuffer, 0, filesize, 0); + FS.close(fs); + + const blobURL = this.createBlobURLFromArrayBuffer(readbuffer.buffer); + + this.descriptionParagraph.innerHTML = ""; + const linkElem = document.createElement("a"); + //let downloadfilename = "solvespace_browser-"; + //downloadfilename += `${GetCurrentDateTimeString()}.slvs`; + let downloadfilename = filename; + linkElem.setAttribute("download", downloadfilename); + linkElem.setAttribute("href", blobURL); + // WORKAROUND: FIXME(emscripten) + linkElem.style.color = "lightblue"; + linkElem.textContent = downloadfilename; + this.descriptionParagraph.appendChild(linkElem); + } + + showDialog() { + this.is_shown = true; + this.modalRoot.style.display = "block"; + } + + closeDialog() { + this.is_shown = false; + this.modalRoot.style.display = "none"; + } +}; + +function saveFileDone(filename, isSaveAs, isAutosave) { + console.log(`saveFileDone(${filename}, ${isSaveAs}, ${isAutosave})`); + if (isAutosave) { + return; + } + const fileDownloadHelper = new FileDownloadHelper(); + fileDownloadHelper.AddButton("OK", 0, true); + fileDownloadHelper.prepareFile(filename); + console.log(`Calling shoDialog()...`); + fileDownloadHelper.showDialog(); + console.log(`shoDialog() finished.`); +} + + +class ScrollbarHelper { + /** + * @param {HTMLElement} elementquery CSS query string for the element that has scrollbar. + */ + constructor(elementquery) { + this.target = document.querySelector(elementquery); + this.rangeMin = 0; + this.rangeMax = 0; + this.currentRatio = 0; + + this.onScrollCallback = null; + this.onScrollCallbackTicking = false; + if (this.target) { + // console.log("addEventListner scroll"); + this.target.parentElement.addEventListener('scroll', () => { + if (this.onScrollCallbackTicking) { + return; + } + window.requestAnimationFrame(() => { + if (this.onScrollCallback) { + this.onScrollCallback(); + } + this.onScrollCallbackTicking = false; + }); + this.onScrollCallbackTicking = true; + }); + } + } + + /** + * + * @param {number} ratio how long against to the viewport height (1.0 to exact same as viewport's height) + */ + setScrollbarSize(ratio) { + // if (isNaN(ratio)) { + // console.warn(`setScrollbarSize(): ratio is Nan = ${ratio}`); + // } + // if (ratio < 0 || ratio > 1) { + // console.warn(`setScrollbarSize(): ratio is out of range 0-1 but ${ratio}`); + // } + // console.log(`ScrollbarHelper.setScrollbarSize(): ratio=${ratio}`); + this.target.style.height = `${100 * ratio}%`; + } + + getScrollbarPosition() { + const scrollbarElem = this.target.parentElement; + const scrollTopMin = 0; + const scrollTopMax = scrollbarElem.scrollHeight - scrollbarElem.clientHeight; + const ratioOnScrollbar = (scrollbarElem.scrollTop - scrollTopMin) / (scrollTopMax - scrollTopMin); + this.currentRatio = (scrollbarElem.scrollTop - scrollTopMin) / (scrollTopMax - scrollTopMin); + let pos = this.currentRatio * (this.rangeMax - this.pageSize - this.rangeMin) + this.rangeMin; + // console.log(`ScrollbarHelper.getScrollbarPosition(): ratio=${ratioOnScrollbar}, pos=${pos}, scrollTop=${scrollbarElem.scrollTop}, scrollTopMin=${scrollTopMin}, scrollTopMax=${scrollTopMax}, rangeMin=${this.rangeMin}, rangeMax=${this.rangeMax}, pageSize=${this.pageSize}`); + if (isNaN(pos)) { + return 0; + } else { + return pos; + } + } + + /** + * @param {number} value in range of rangeMin and rangeMax + */ + setScrollbarPosition(position) { + const positionMin = this.rangeMin; + const positionMax = this.rangeMax - this.pageSize; + const currentPositionRatio = (position - positionMin) / (positionMax - positionMin); + + const scrollbarElement = this.target.parentElement; + const scrollTopMin = 0; + const scrollTopMax = scrollbarElement.scrollHeight - scrollbarElement.clientHeight; + const scrollWidth = scrollTopMax - scrollTopMin; + const newScrollTop = currentPositionRatio * scrollWidth; + scrollbarElement.scrollTop = currentPositionRatio * scrollWidth; + + // console.log(`ScrollbarHelper.setScrollbarPosition(): pos=${position}, currentPositionRatio=${currentPositionRatio}, calculated scrollTop=${newScrollTop}`); + + if (false) { + // const ratio = (position - this.rangeMin) * ((this.rangeMax - this.pageSize) - this.rangeMin); + + const scrollTopMin = 0; + const scrollTopMax = this.target.scrollHeight - this.target.clientHeight; + const scrollWidth = scrollTopMax - scrollTopMin; + const newScrollTop = ratio * scrollWidth; + // this.target.parentElement.scrollTop = ratio * scrollWidth; + this.target.scrollTop = ratio * scrollWidth; + + console.log(`ScrollbarHelper.setScrollbarPosition(): pos=${position}, ratio=${ratio}, calculated scrollTop=${newScrollTop}`); + } + } + + /** */ + setRange(min, max, pageSize) { + this.rangeMin = min; + this.rangeMax = max; + this.currentRatio = 0; + + this.setPageSize(pageSize); + } + + setPageSize(pageSize) { + if (this.rangeMin == this.rangeMax) { + // console.log(`ScrollbarHelper::setPageSize(): size=${size}, but rangeMin == rangeMax`); + return; + } + this.pageSize = pageSize; + const ratio = (this.rangeMax - this.rangeMin) / this.pageSize; + // console.log(`ScrollbarHelper::setPageSize(): pageSize=${pageSize}, ratio=${ratio}`); + this.setScrollbarSize(ratio); + } + + setScrollbarEnabled(enabled) { + if (!enabled) { + this.target.style.height = "100%"; + } + } +}; + +window.ScrollbarHelper = ScrollbarHelper; diff --git a/src/platform/platform.cpp b/src/platform/platform.cpp new file mode 100644 index 0000000..a57ff5e --- /dev/null +++ b/src/platform/platform.cpp @@ -0,0 +1,657 @@ +//----------------------------------------------------------------------------- +// Platform-dependent functionality. +// +// Copyright 2017 whitequark +//----------------------------------------------------------------------------- +#if defined(__APPLE__) +// Include Apple headers before solvespace.h to avoid identifier clashes. +# include +# include +# include +#endif + +#include +#include +#include + +#include + +#if defined(WIN32) +// Conversely, include Microsoft headers after solvespace.h to avoid clashes. +# include +# include +#else +# include +# include +#endif + +#include "platform.h" +#include "util.h" +#include "config.h" + +namespace SolveSpace { +namespace Platform { + +//----------------------------------------------------------------------------- +// UTF-8 ⟷ UTF-16 conversion, on Windows. +//----------------------------------------------------------------------------- + +#if defined(WIN32) + +std::string Narrow(const wchar_t *in) +{ + std::string out; + DWORD len = WideCharToMultiByte(CP_UTF8, 0, in, -1, NULL, 0, NULL, NULL); + out.resize(len - 1); + ssassert(WideCharToMultiByte(CP_UTF8, 0, in, -1, &out[0], len, NULL, NULL), + "Invalid UTF-16"); + return out; +} + +std::string Narrow(const std::wstring &in) +{ + if(in == L"") return ""; + + std::string out; + out.resize(WideCharToMultiByte(CP_UTF8, 0, &in[0], (int)in.length(), + NULL, 0, NULL, NULL)); + ssassert(WideCharToMultiByte(CP_UTF8, 0, &in[0], (int)in.length(), + &out[0], (int)out.length(), NULL, NULL), + "Invalid UTF-16"); + return out; +} + +std::wstring Widen(const char *in) +{ + std::wstring out; + DWORD len = MultiByteToWideChar(CP_UTF8, 0, in, -1, NULL, 0); + out.resize(len - 1); + ssassert(MultiByteToWideChar(CP_UTF8, 0, in, -1, &out[0], len), + "Invalid UTF-8"); + return out; +} + +std::wstring Widen(const std::string &in) +{ + if(in == "") return L""; + + std::wstring out; + out.resize(MultiByteToWideChar(CP_UTF8, 0, &in[0], (int)in.length(), NULL, 0)); + ssassert(MultiByteToWideChar(CP_UTF8, 0, &in[0], (int)in.length(), + &out[0], (int)out.length()), + "Invalid UTF-8"); + return out; +} + +#endif + +//----------------------------------------------------------------------------- +// Path utility functions. +//----------------------------------------------------------------------------- + +static std::vector Split(const std::string &joined, char separator) { + std::vector parts; + + size_t oldpos = 0, pos = 0; + while(true) { + oldpos = pos; + pos = joined.find(separator, pos); + if(pos == std::string::npos) break; + parts.push_back(joined.substr(oldpos, pos - oldpos)); + pos += 1; + } + + parts.push_back(joined.substr(oldpos)); + + return parts; +} + +static std::string Concat(const std::vector &parts, char separator) { + std::string joined; + + bool first = true; + for(auto &part : parts) { + if(!first) joined += separator; + joined += part; + first = false; + } + + return joined; +} + +//----------------------------------------------------------------------------- +// Path manipulation. +//----------------------------------------------------------------------------- + +#if defined(WIN32) +const char SEPARATOR = '\\'; +#else +const char SEPARATOR = '/'; +#endif + +Path Path::From(std::string raw) { + Path path = { raw }; + return path; +} + +Path Path::CurrentDirectory() { +#if defined(WIN32) + // On Windows, OpenFile needs an absolute UNC path proper, so get that. + std::wstring rawW; + rawW.resize(GetCurrentDirectoryW(0, NULL)); + DWORD length = GetCurrentDirectoryW((int)rawW.length(), &rawW[0]); + ssassert(length > 0 && length == rawW.length() - 1, "Cannot get current directory"); + rawW.resize(length); + return From(Narrow(rawW)); +#else + char *raw = getcwd(NULL, 0); + ssassert(raw != NULL, "Cannot get current directory"); + Path path = From(raw); + free(raw); + return path; +#endif +} + +std::string Path::FileName() const { + std::string fileName = raw; + size_t slash = fileName.rfind(SEPARATOR); + if(slash != std::string::npos) { + fileName = fileName.substr(slash + 1); + } + return fileName; +} + +std::string Path::FileStem() const { + std::string baseName = FileName(); + size_t dot = baseName.rfind('.'); + if(dot != std::string::npos) { + baseName = baseName.substr(0, dot); + } + return baseName; +} + +std::string Path::Extension() const { + size_t dot = raw.rfind('.'); + if(dot != std::string::npos) { + return raw.substr(dot + 1); + } + return ""; +} + +bool Path::HasExtension(std::string theirExt) const { + std::string ourExt = Extension(); + std::transform(ourExt.begin(), ourExt.end(), ourExt.begin(), ::tolower); + std::transform(theirExt.begin(), theirExt.end(), theirExt.begin(), ::tolower); + return ourExt == theirExt; +} + +Path Path::WithExtension(std::string ext) const { + Path withExt = *this; + size_t dot = withExt.raw.rfind('.'); + if(dot != std::string::npos) { + withExt.raw.erase(dot); + } + if(!ext.empty()) { + withExt.raw += "."; + withExt.raw += ext; + } + return withExt; +} + +static void FindPrefix(const std::string &raw, size_t *pos) { + *pos = std::string::npos; +#if defined(WIN32) + if(raw.size() >= 7 && raw[2] == '?' && raw[3] == '\\' && + isalpha(raw[4]) && raw[5] == ':' && raw[6] == '\\') { + *pos = 7; + } else if(raw.size() >= 3 && isalpha(raw[0]) && raw[1] == ':' && raw[2] == '\\') { + *pos = 3; + } else if(raw.size() >= 2 && raw[0] == '\\' && raw[1] == '\\') { + size_t slashAt = raw.find('\\', 2); + if(slashAt != std::string::npos) { + *pos = raw.find('\\', slashAt + 1); + } + } +#else + if(!raw.empty() && raw[0] == '/') { + *pos = 1; + } +#endif +} + +bool Path::IsAbsolute() const { + size_t pos; + FindPrefix(raw, &pos); + return pos != std::string::npos; +} + +// Removes one component from the end of the path. +// Returns an empty path if the path consists only of a root. +Path Path::Parent() const { + Path parent = { raw }; + if(!parent.raw.empty() && parent.raw.back() == SEPARATOR) { + parent.raw.pop_back(); + } + size_t slash = parent.raw.rfind(SEPARATOR); + if(slash != std::string::npos) { + parent.raw = parent.raw.substr(0, slash + 1); + } else { + parent.raw.clear(); + } + if(IsAbsolute() && !parent.IsAbsolute()) { + return From(""); + } + return parent; +} + +// Concatenates a component to this path. +// Returns a relative path if this path is empty. +// Returns an empty path if the component is absolute. +Path Path::Join(const std::string &component) const { + ssassert(component.find(SEPARATOR) == std::string::npos, + "Use the Path::Join(const Path &) overload to append an entire path"); + return Join(Path::From(component)); +} + +// Concatenates a relative path to this path. +// Returns a relative path if this path is empty. +// Returns an empty path if the other path is absolute. +Path Path::Join(const Path &other) const { + if(other.IsAbsolute()) { + return From(""); + } + + Path joined; + if(IsEmpty()) { + joined.raw = "."; + } else { + joined.raw = raw; + } + + if(joined.raw.back() != SEPARATOR) { + joined.raw += SEPARATOR; + } + joined.raw += other.raw; + return joined; +} + +// Expands the "." and ".." components in this path. +// On Windows, additionally prepends the UNC prefix to absolute paths without one. +// Returns an empty path if a ".." component would escape from the root. +Path Path::Expand(bool fromCurrentDirectory) const { + Path source; + Path expanded; + + if(fromCurrentDirectory && !IsAbsolute()) { + source = CurrentDirectory().Join(*this); + } else { + source = *this; + } + + size_t splitAt; + FindPrefix(source.raw, &splitAt); + if(splitAt != std::string::npos) { + expanded.raw = source.raw.substr(0, splitAt); + } else { + splitAt = 0; + } + + std::vector expandedComponents; + for(std::string component : Split(source.raw.substr(splitAt), SEPARATOR)) { + if(component == ".") { + // skip + } else if(component == "..") { + if(!expandedComponents.empty()) { + expandedComponents.pop_back(); + } else { + return From(""); + } + } else if(!component.empty()) { + expandedComponents.push_back(component); + } + } + + if(expanded.IsEmpty()) { + if(expandedComponents.empty()) { + expandedComponents.emplace_back("."); + } + expanded = From(Concat(expandedComponents, SEPARATOR)); + } else if(!expandedComponents.empty()) { + expanded = expanded.Join(From(Concat(expandedComponents, SEPARATOR))); + } + +#if defined(WIN32) + if(expanded.IsAbsolute() && expanded.raw.substr(0, 2) != "\\\\") { + expanded.raw = "\\\\?\\" + expanded.raw; + } +#endif + + return expanded; +} + +static std::string FilesystemNormalize(const std::string &str) { +#if defined(WIN32) + std::wstring strW = Widen(str); + std::transform(strW.begin(), strW.end(), strW.begin(), towlower); + return Narrow(strW); +#elif defined(__APPLE__) + CFMutableStringRef cfStr = + CFStringCreateMutableCopy(NULL, 0, + CFStringCreateWithBytesNoCopy(NULL, (const UInt8*)str.data(), str.size(), + kCFStringEncodingUTF8, /*isExternalRepresentation=*/false, kCFAllocatorNull)); + CFStringLowercase(cfStr, NULL); + std::string normalizedStr; + normalizedStr.resize(CFStringGetMaximumSizeOfFileSystemRepresentation(cfStr)); + CFStringGetFileSystemRepresentation(cfStr, &normalizedStr[0], normalizedStr.size()); + normalizedStr.erase(normalizedStr.find('\0')); + return normalizedStr; +#else + return str; +#endif +} + +bool Path::Equals(const Path &other) const { + return FilesystemNormalize(raw) == FilesystemNormalize(other.raw); +} + +// Returns a relative path from a given base path. +// Returns an empty path if any of the paths is not absolute, or +// if they belong to different roots, or +// if they cannot be expanded. +Path Path::RelativeTo(const Path &base) const { + Path expanded = Expand(); + Path baseExpanded = base.Expand(); + if(!(expanded.IsAbsolute() && baseExpanded.IsAbsolute())){ + return From(""); + } + + size_t splitAt; + FindPrefix(expanded.raw, &splitAt); + size_t baseSplitAt; + FindPrefix(baseExpanded.raw, &baseSplitAt); + if(FilesystemNormalize(expanded.raw.substr(0, splitAt)) != + FilesystemNormalize(baseExpanded.raw.substr(0, splitAt))) { + return From(""); + } + + std::vector components = + Split(expanded.raw.substr(splitAt), SEPARATOR); + std::vector baseComponents = + Split(baseExpanded.raw.substr(baseSplitAt), SEPARATOR); + size_t common; + for(common = 0; common < baseComponents.size() && + common < components.size(); common++) { + if(FilesystemNormalize(baseComponents[common]) != + FilesystemNormalize(components[common])) { + break; + } + } + + std::vector resultComponents; + for(size_t i = common; i < baseComponents.size(); i++) { + resultComponents.emplace_back(".."); + } + resultComponents.insert(resultComponents.end(), + components.begin() + common, components.end()); + if(resultComponents.empty()) { + resultComponents.emplace_back("."); + } + return From(Concat(resultComponents, SEPARATOR)); +} + +Path Path::FromPortable(const std::string &repr) { + return From(Concat(Split(repr, '/'), SEPARATOR)); +} + +std::string Path::ToPortable() const { + ssassert(!IsAbsolute(), "absolute paths cannot be made portable"); + + return Concat(Split(raw, SEPARATOR), '/'); +} + +//----------------------------------------------------------------------------- +// File manipulation. +//----------------------------------------------------------------------------- + +FILE *OpenFile(const Platform::Path &filename, const char *mode) { + ssassert(filename.raw.length() == strlen(filename.raw.c_str()), + "Unexpected null byte in middle of a path"); +#if defined(WIN32) + return _wfopen(Widen(filename.Expand(/*fromCurrentDirectory=*/true).raw).c_str(), Widen(mode).c_str()); +#else + return fopen(filename.raw.c_str(), mode); +#endif +} + +bool FileExists(const Platform::Path &filename) { + FILE *f = OpenFile(filename, "rb"); + if(f == NULL) return false; + fclose(f); + return true; +} + +void RemoveFile(const Platform::Path &filename) { + ssassert(filename.raw.length() == strlen(filename.raw.c_str()), + "Unexpected null byte in middle of a path"); +#if defined(WIN32) + _wremove(Widen(filename.Expand().raw).c_str()); +#else + remove(filename.raw.c_str()); +#endif +} + +bool ReadFile(const Platform::Path &filename, std::string *data) { + FILE *f = OpenFile(filename, "rb"); + if(f == NULL) return false; + + if(fseek(f, 0, SEEK_END) != 0) + return false; + data->resize(ftell(f)); + if(fseek(f, 0, SEEK_SET) != 0) + return false; + if(fread(&(*data)[0], 1, data->size(), f) != data->size()) + return false; + if(fclose(f) != 0) + return false; + + return true; +} + +bool WriteFile(const Platform::Path &filename, const std::string &data) { + FILE *f = OpenFile(filename, "wb"); + if(f == NULL) return false; + + if(fwrite(&data[0], 1, data.size(), f) != data.size()) + return false; + if(fclose(f) != 0) + return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Loading resources, on Windows. +//----------------------------------------------------------------------------- + +#if defined(WIN32) + +const void *LoadResource(const std::string &name, size_t *size) { + HRSRC hres = FindResourceW(NULL, Widen(name).c_str(), RT_RCDATA); + ssassert(hres != NULL, "Cannot find resource"); + HGLOBAL res = ::LoadResource(NULL, hres); + ssassert(res != NULL, "Cannot load resource"); + + *size = SizeofResource(NULL, hres); + return LockResource(res); +} + +#endif + +//----------------------------------------------------------------------------- +// Loading resources, on *nix. +//----------------------------------------------------------------------------- + +#if defined(__APPLE__) + +static Platform::Path PathFromCFURL(CFURLRef cfUrl) { + Path path; + CFStringRef cfPath = CFURLCopyFileSystemPath(cfUrl, kCFURLPOSIXPathStyle); + path.raw.resize(CFStringGetMaximumSizeOfFileSystemRepresentation(cfPath)); + CFStringGetFileSystemRepresentation(cfPath, &path.raw[0], path.raw.size()); + path.raw.erase(path.raw.find('\0')); + CFRelease(cfPath); + return path; +} + +static Platform::Path ResourcePath(const std::string &name) { + Path path; + + // First, try to get the URL from the bundle. + CFStringRef cfName = CFStringCreateWithCString(kCFAllocatorDefault, name.c_str(), + kCFStringEncodingUTF8); + CFURLRef cfUrl = CFBundleCopyResourceURL(CFBundleGetMainBundle(), cfName, NULL, NULL); + if(cfUrl != NULL) { + path = PathFromCFURL(cfUrl); + CFRelease(cfUrl); + } + CFRelease(cfName); + + if(!path.IsEmpty()) return path; + + // If that failed, it means we aren't running from the bundle. + // Reference off the executable path, then. + cfUrl = CFBundleCopyExecutableURL(CFBundleGetMainBundle()); + if(cfUrl != NULL) { + path = PathFromCFURL(cfUrl).Parent().Parent().Join("res"); + path = path.Join(Path::FromPortable(name)); + CFRelease(cfUrl); + } + + return path; +} + +#elif defined(__EMSCRIPTEN__) + +static Platform::Path ResourcePath(const std::string &name) { + return Path::From("res/" + name); +} + +#elif !defined(WIN32) + +# if defined(__linux__) +static const char *selfSymlink = "/proc/self/exe"; +# elif defined(__NetBSD__) +static const char *selfSymlink = "/proc/curproc/exe"; +# elif defined(__OpenBSD__) || defined(__FreeBSD__) +static const char *selfSymlink = "/proc/curproc/file"; +# else +static const char *selfSymlink = ""; +# endif + +static Platform::Path FindLocalResourceDir() { + // Find out the path to the running binary. + Platform::Path selfPath; + char *expandedSelfPath = realpath(selfSymlink, NULL); + if(expandedSelfPath != NULL) { + selfPath = Path::From(expandedSelfPath); + } + free(expandedSelfPath); + + Platform::Path resourceDir; + if(selfPath.IsEmpty()) { + // We don't know how to find the local resource directory on this platform, + // so use the global one (by returning an empty string). + return Path::From(UNIX_DATADIR); + } else { + resourceDir = selfPath.Parent().Parent().Join("res"); + } + + struct stat st; + if(stat(resourceDir.raw.c_str(), &st) != -1) { + // An executable-adjacent resource directory exists, good. + return resourceDir; + } + + resourceDir = selfPath.Parent().Parent().Join("share").Join("solvespace"); + if(stat(resourceDir.raw.c_str(), &st) != -1) { + // A resource directory exists at a relative path, good. + return resourceDir; + } + + // No executable-adjacent resource directory; use the one from compile-time prefix. + return Path::From(UNIX_DATADIR); +} + +static Platform::Path ResourcePath(const std::string &name) { + static Platform::Path resourceDir; + if(resourceDir.IsEmpty()) { + resourceDir = FindLocalResourceDir(); + } + + return resourceDir.Join(Path::FromPortable(name)); +} + +#endif + +#if !defined(WIN32) + +const void *LoadResource(const std::string &name, size_t *size) { + static std::map cache; + + auto it = cache.find(name); + if(it == cache.end()) { + ssassert(ReadFile(ResourcePath(name), &cache[name]), "Cannot read resource"); + it = cache.find(name); + } + + const std::string &content = (*it).second; + *size = content.size(); + return (const void*)content.data(); +} + +#endif + +//----------------------------------------------------------------------------- +// Startup and command-line argument handling, on Windows. +//----------------------------------------------------------------------------- + +#if defined(WIN32) + +std::vector InitCli(int argc, char **argv) { +#if defined(_MSC_VER) + // We display our own message on abort; just call ReportFault. + _set_abort_behavior(_CALL_REPORTFAULT, _WRITE_ABORT_MSG|_CALL_REPORTFAULT); + int crtReportTypes[] = {_CRT_WARN, _CRT_ERROR, _CRT_ASSERT}; + for(int crtReportType : crtReportTypes) { + _CrtSetReportMode(crtReportType, _CRTDBG_MODE_FILE|_CRTDBG_MODE_DEBUG); + _CrtSetReportFile(crtReportType, _CRTDBG_FILE_STDERR); + } +#endif + + // Extract the command-line arguments; the ones from main() are ignored, + // since they are in the OEM encoding. + int argcW; + LPWSTR *argvW = CommandLineToArgvW(GetCommandLineW(), &argcW); + std::vector args; + for(int i = 0; i < argcW; i++) + args.push_back(Platform::Narrow(argvW[i])); + LocalFree(argvW); + return args; +} + +#endif + +//----------------------------------------------------------------------------- +// Startup and command-line argument handling, on *nix. +//----------------------------------------------------------------------------- + +#if !defined(WIN32) + +std::vector InitCli(int argc, char **argv) { + return {&argv[0], &argv[argc]}; +} + +#endif + +} +} diff --git a/src/platform/platform.h b/src/platform/platform.h new file mode 100644 index 0000000..771a4cf --- /dev/null +++ b/src/platform/platform.h @@ -0,0 +1,87 @@ +//----------------------------------------------------------------------------- +// Platform-dependent functionality. +// +// Copyright 2017 whitequark +//----------------------------------------------------------------------------- + +#ifndef SOLVESPACE_PLATFORM_H +#define SOLVESPACE_PLATFORM_H + +#include +#include + +namespace SolveSpace { +namespace Platform { + +// UTF-8 ⟷ UTF-16 conversion, for Windows. +#if defined(WIN32) +std::string Narrow(const wchar_t *s); +std::wstring Widen(const char *s); +std::string Narrow(const std::wstring &s); +std::wstring Widen(const std::string &s); +#endif + +#if defined(_WIN32) + const std::string embeddedFont = "res://fonts/BitstreamVeraSans-Roman-builtin.ttf"; +#else // Linux and macOS + const std::string embeddedFont = "BitstreamVeraSans-Roman-builtin.ttf"; +#endif + +// A filesystem path, respecting the conventions of the current platform. +// Transformation functions return an empty path on error. +class Path { +public: + std::string raw; + + static Path From(std::string raw); + static Path CurrentDirectory(); + + void Clear() { raw.clear(); } + + bool Equals(const Path &other) const; + bool IsEmpty() const { return raw.empty(); } + bool IsAbsolute() const; + bool HasExtension(std::string ext) const; + + std::string FileName() const; + std::string FileStem() const; + std::string Extension() const; + + Path WithExtension(std::string ext) const; + Path Parent() const; + Path Join(const std::string &component) const; + Path Join(const Path &other) const; + Path Expand(bool fromCurrentDirectory = false) const; + Path RelativeTo(const Path &base) const; + + // Converting to and from a platform-independent representation + // (conventionally, the Unix one). + static Path FromPortable(const std::string &repr); + std::string ToPortable() const; +}; + +struct PathLess { + bool operator()(const Path &a, const Path &b) const { return a.raw < b.raw; } +}; + +// File manipulation functions. +bool FileExists(const Platform::Path &filename); +FILE *OpenFile(const Platform::Path &filename, const char *mode); +bool ReadFile(const Platform::Path &filename, std::string *data); +bool WriteFile(const Platform::Path &filename, const std::string &data); +void RemoveFile(const Platform::Path &filename); + +// Resource loading function. +const void *LoadResource(const std::string &name, size_t *size); + +// Startup and command-line argument handling. +std::vector InitCli(int argc, char **argv); + +// Temporary arena functions. +void *AllocTemporary(size_t size); +void FreeAllTemporary(); + +} // namespace Platform +} // namespace SolveSpace + +#endif diff --git a/src/platform/platformbase.cpp b/src/platform/platformbase.cpp new file mode 100644 index 0000000..6e3bb8a --- /dev/null +++ b/src/platform/platformbase.cpp @@ -0,0 +1,101 @@ +#include +#include + +#include + +#if defined(WIN32) +# include +#endif // defined(WIN32) + +#include "util.h" +#include "platform.h" + +namespace SolveSpace { +namespace Platform { + +//----------------------------------------------------------------------------- +// Debug output, on Windows. +//----------------------------------------------------------------------------- + +#if defined(WIN32) + +#if !defined(_alloca) +// Fix for compiling with MinGW.org GCC-6.3.0-1 +#define _alloca alloca +#include +#endif + +void DebugPrint(const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + int len = _vscprintf(fmt, va) + 1; + va_end(va); + + va_start(va, fmt); + char *buf = (char *)_alloca(len); + _vsnprintf(buf, len, fmt, va); + va_end(va); + + // The native version of OutputDebugString, unlike most others, + // is OutputDebugStringA. + OutputDebugStringA(buf); + OutputDebugStringA("\n"); + +#ifndef NDEBUG + // Duplicate to stderr in debug builds, but not in release; this is slow. + fputs(buf, stderr); + fputc('\n', stderr); +#endif +} + +#endif + +//----------------------------------------------------------------------------- +// Debug output, on *nix. +//----------------------------------------------------------------------------- + +#if !defined(WIN32) + +void DebugPrint(const char *fmt, ...) { + va_list va; + va_start(va, fmt); + vfprintf(stderr, fmt, va); + fputc('\n', stderr); + va_end(va); +} + +#endif + +//----------------------------------------------------------------------------- +// Temporary arena. +//----------------------------------------------------------------------------- + +struct MimallocHeap { + mi_heap_t *heap = NULL; + + ~MimallocHeap() { + if(heap != NULL) + mi_heap_destroy(heap); + } +}; + +static thread_local MimallocHeap TempArena; + +void *AllocTemporary(size_t size) { + if(TempArena.heap == NULL) { + TempArena.heap = mi_heap_new(); + ssassert(TempArena.heap != NULL, "out of memory"); + } + void *ptr = mi_heap_zalloc(TempArena.heap, size); + ssassert(ptr != NULL, "out of memory"); + return ptr; +} + +void FreeAllTemporary() { + MimallocHeap temp; + std::swap(TempArena.heap, temp.heap); +} + +} +} diff --git a/src/polygon.cpp b/src/polygon.cpp new file mode 100644 index 0000000..211f6a6 --- /dev/null +++ b/src/polygon.cpp @@ -0,0 +1,924 @@ +//----------------------------------------------------------------------------- +// Operations on polygons (planar, of line segment edges). +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +Vector STriangle::Normal() const { + Vector ab = b.Minus(a), bc = c.Minus(b); + return ab.Cross(bc); +} + +double STriangle::MinAltitude() const { + double altA = a.DistanceToLine(b, c.Minus(b)), + altB = b.DistanceToLine(c, a.Minus(c)), + altC = c.DistanceToLine(a, b.Minus(a)); + + return min(altA, min(altB, altC)); +} + +bool STriangle::ContainsPoint(Vector p) const { + Vector n = Normal(); + if(MinAltitude() < LENGTH_EPS) { + // shouldn't happen; zero-area triangle + return false; + } + return ContainsPointProjd(n.WithMagnitude(1), p); +} + +bool STriangle::ContainsPointProjd(Vector n, Vector p) const { + Vector ab = b.Minus(a), bc = c.Minus(b), ca = a.Minus(c); + + Vector no_ab = n.Cross(ab); + if(no_ab.Dot(p) < no_ab.Dot(a) - LENGTH_EPS) return false; + + Vector no_bc = n.Cross(bc); + if(no_bc.Dot(p) < no_bc.Dot(b) - LENGTH_EPS) return false; + + Vector no_ca = n.Cross(ca); + if(no_ca.Dot(p) < no_ca.Dot(c) - LENGTH_EPS) return false; + + return true; +} + +bool STriangle::Raytrace(const Vector &rayPoint, const Vector &rayDir, + double *t, Vector *inters) const { + // Algorithm from: "Fast, Minimum Storage Ray/Triangle Intersection" by + // Tomas Moeller and Ben Trumbore. + + // Find vectors for two edges sharing vertex A. + Vector edge1 = b.Minus(a); + Vector edge2 = c.Minus(a); + + // Begin calculating determinant - also used to calculate U parameter. + Vector pvec = rayDir.Cross(edge2); + + // If determinant is near zero, ray lies in plane of triangle. + // Also, cull back facing triangles here. + double det = edge1.Dot(pvec); + if(-det < LENGTH_EPS) return false; + double inv_det = 1.0f / det; + + // Calculate distance from vertex A to ray origin. + Vector tvec = rayPoint.Minus(a); + + // Calculate U parameter and test bounds. + double u = tvec.Dot(pvec) * inv_det; + if (u < 0.0f || u > 1.0f) return false; + + // Prepare to test V parameter. + Vector qvec = tvec.Cross(edge1); + + // Calculate V parameter and test bounds. + double v = rayDir.Dot(qvec) * inv_det; + if (v < 0.0f || u + v > 1.0f) return false; + + // Calculate t, ray intersects triangle. + *t = edge2.Dot(qvec) * inv_det; + + // Calculate intersection point. + if(inters != NULL) *inters = rayPoint.Plus(rayDir.ScaledBy(*t)); + + return true; +} + +double STriangle::SignedVolume() const { + return a.Dot(b.Cross(c)) / 6.0; +} + +double STriangle::Area() const { + Vector ab = a.Minus(b); + Vector cb = c.Minus(b); + return ab.Cross(cb).Magnitude() / 2.0; +} + +bool STriangle::IsDegenerate() const { + return a.OnLineSegment(b, c) || + b.OnLineSegment(a, c) || + c.OnLineSegment(a, b); +} + +void STriangle::FlipNormal() { + swap(a, b); + swap(an, bn); +} + +STriangle STriangle::Transform(Vector u, Vector v, Vector n) const { + STriangle tr = *this; + tr.a = tr.a.ScaleOutOfCsys(u, v, n); + tr.an = tr.an.ScaleOutOfCsys(u, v, n); + tr.b = tr.b.ScaleOutOfCsys(u, v, n); + tr.bn = tr.bn.ScaleOutOfCsys(u, v, n); + tr.c = tr.c.ScaleOutOfCsys(u, v, n); + tr.cn = tr.cn.ScaleOutOfCsys(u, v, n); + return tr; +} + +STriangle STriangle::From(STriMeta meta, Vector a, Vector b, Vector c) { + STriangle tr = {}; + tr.meta = meta; + tr.a = a; + tr.b = b; + tr.c = c; + return tr; +} + +SEdge SEdge::From(Vector a, Vector b) { + SEdge se = {}; + se.a = a; + se.b = b; + return se; +} + +bool SEdge::EdgeCrosses(Vector ea, Vector eb, Vector *ppi, SPointList *spl) const { + Vector d = eb.Minus(ea); + double t_eps = LENGTH_EPS/d.Magnitude(); + + double t, tthis; + bool skew; + Vector pi; + bool inOrEdge0, inOrEdge1; + + Vector dthis = b.Minus(a); + double tthis_eps = LENGTH_EPS/dthis.Magnitude(); + + if((ea.Equals(a) && eb.Equals(b)) || + (eb.Equals(a) && ea.Equals(b))) + { + if(ppi) *ppi = a; + if(spl) spl->Add(a); + return true; + } + + // Can't just test if distance between d and a equals distance between d and b; + // they could be on opposite sides, since we don't have the sign. + double m = sqrt(d.Magnitude()*dthis.Magnitude()); + if(sqrt(fabs(d.Dot(dthis))) > (m - LENGTH_EPS)) { + // The edges are parallel. + if(fabs(a.DistanceToLine(ea, d)) > LENGTH_EPS) { + // and not coincident, so can't be intersecting + return false; + } + // The edges are coincident. Make sure that neither endpoint lies + // on the other + bool inters = false; + double t; + t = a.Minus(ea).DivProjected(d); + if(t > t_eps && t < (1 - t_eps)) inters = true; + t = b.Minus(ea).DivProjected(d); + if(t > t_eps && t < (1 - t_eps)) inters = true; + t = ea.Minus(a).DivProjected(dthis); + if(t > tthis_eps && t < (1 - tthis_eps)) inters = true; + t = eb.Minus(a).DivProjected(dthis); + if(t > tthis_eps && t < (1 - tthis_eps)) inters = true; + + if(inters) { + if(ppi) *ppi = a; + if(spl) spl->Add(a); + return true; + } else { + // So coincident but disjoint, okay. + return false; + } + } + + // Lines are not parallel, so look for an intersection. + pi = Vector::AtIntersectionOfLines(ea, eb, a, b, + &skew, + &t, &tthis); + if(skew) return false; + + inOrEdge0 = (t > -t_eps) && (t < (1 + t_eps)); + inOrEdge1 = (tthis > -tthis_eps) && (tthis < (1 + tthis_eps)); + + if(inOrEdge0 && inOrEdge1) { + if(a.Equals(ea) || b.Equals(ea) || + a.Equals(eb) || b.Equals(eb)) + { + // Not an intersection if we share an endpoint with an edge + return false; + } + // But it's an intersection if a vertex of one edge lies on the + // inside of the other (or if they cross away from either's + // vertex). + if(ppi) *ppi = pi; + if(spl) spl->Add(pi); + return true; + } + return false; +} + +void SEdgeList::Clear() { + l.Clear(); +} + +void SEdgeList::AddEdge(Vector a, Vector b, int auxA, int auxB, int tag) { + SEdge e = {}; + e.tag = tag; + e.a = a; + e.b = b; + e.auxA = auxA; + e.auxB = auxB; + l.Add(&e); +} + +bool SEdgeList::AssembleContour(Vector first, Vector last, SContour *dest, + SEdge *errorAt, bool keepDir, int start) const +{ + int i; + + dest->AddPoint(first); + dest->AddPoint(last); + + do { + for(i = start; i < l.n; i++) { + /// @todo fix const! + SEdge *se = const_cast(&(l[i])); + if(se->tag) continue; + + if(se->a.Equals(last)) { + dest->AddPoint(se->b); + last = se->b; + se->tag = 1; + break; + } + // Don't allow backwards edges if keepDir is true. + if(!keepDir && se->b.Equals(last)) { + dest->AddPoint(se->a); + last = se->a; + se->tag = 1; + break; + } + } + if(i >= l.n) { + // Couldn't assemble a closed contour; mark where. + if(errorAt) { + errorAt->a = first; + errorAt->b = last; + } + return false; + } + + } while(!last.Equals(first)); + + return true; +} + +bool SEdgeList::AssemblePolygon(SPolygon *dest, SEdge *errorAt, bool keepDir) const { + dest->Clear(); + + bool allClosed = true; + Vector first = Vector::From(0, 0, 0); + Vector last = Vector::From(0, 0, 0); + int i; + for(i = 0; i < l.n; i++) { + if(!l[i].tag) { + first = l[i].a; + last = l[i].b; + /// @todo fix const! + const_cast(&(l[i]))->tag = 1; + // Create a new empty contour in our polygon, and finish assembling + // into that contour. + dest->AddEmptyContour(); + if(!AssembleContour(first, last, dest->l.Last(), errorAt, keepDir, i+1)) { + allClosed = false; + } + // But continue assembling, even if some of the contours are open + } + } + return allClosed; +} + +//----------------------------------------------------------------------------- +// Test if the specified edge crosses any of the edges in our list. Two edges +// are not considered to cross if they share an endpoint (within LENGTH_EPS), +// but they are considered to cross if they are coincident and overlapping. +// If pi is not NULL, then a crossing is returned in that. +//----------------------------------------------------------------------------- +int SEdgeList::AnyEdgeCrossings(Vector a, Vector b, Vector *ppi, SPointList *spl) const { + auto cnt = std::count_if(l.begin(), l.end(), + [&](SEdge const &se) { return se.EdgeCrosses(a, b, ppi, spl); }); + return static_cast(cnt); +} + +//----------------------------------------------------------------------------- +// Returns true if the intersecting edge list contains an edge that shares +// an endpoint with one of our edges. +//----------------------------------------------------------------------------- +bool SEdgeList::ContainsEdgeFrom(const SEdgeList *sel) const { + for(const SEdge *se = l.First(); se; se = l.NextAfter(se)) { + if(sel->ContainsEdge(se)) return true; + } + return false; +} +bool SEdgeList::ContainsEdge(const SEdge *set) const { + for(const SEdge *se = l.First(); se; se = l.NextAfter(se)) { + if((se->a).Equals(set->a) && (se->b).Equals(set->b)) return true; + if((se->b).Equals(set->a) && (se->a).Equals(set->b)) return true; + } + return false; +} + +//----------------------------------------------------------------------------- +// Remove unnecessary edges: +// - if two are anti-parallel then +// if both=true, remove both +// else remove only one. +// - if two are parallel then remove one. +//----------------------------------------------------------------------------- +void SEdgeList::CullExtraneousEdges(bool both) { + l.ClearTags(); + for(int i = 0; i < l.n; i++) { + SEdge *se = &(l[i]); + for(int j = i + 1; j < l.n; j++) { + SEdge *set = &(l[j]); + if((set->a).Equals(se->a) && (set->b).Equals(se->b)) { + // Two parallel edges exist; so keep only the first one. + set->tag = 1; + } + if((set->a).Equals(se->b) && (set->b).Equals(se->a)) { + // Two anti-parallel edges exist; if both=true, keep neither, + // otherwise keep only one. + if (both) se->tag = 1; + set->tag = 1; + } + } + } + l.RemoveTagged(); +} + +//----------------------------------------------------------------------------- +// Make a kd-tree of edges. This is used for O(log(n)) implementations of stuff +// that would naively be O(n). +//----------------------------------------------------------------------------- +SKdNodeEdges *SKdNodeEdges::Alloc() { + SKdNodeEdges *ne = (SKdNodeEdges *)Platform::AllocTemporary(sizeof(SKdNodeEdges)); + *ne = {}; + return ne; +} +SEdgeLl *SEdgeLl::Alloc() { + SEdgeLl *sell = (SEdgeLl *)Platform::AllocTemporary(sizeof(SEdgeLl)); + *sell = {}; + return sell; +} +SKdNodeEdges *SKdNodeEdges::From(SEdgeList *sel) { + SEdgeLl *sell = NULL; + SEdge *se; + for(se = sel->l.First(); se; se = sel->l.NextAfter(se)) { + SEdgeLl *n = SEdgeLl::Alloc(); + n->se = se; + n->next = sell; + sell = n; + } + return SKdNodeEdges::From(sell); +} +SKdNodeEdges *SKdNodeEdges::From(SEdgeLl *sell) { + SKdNodeEdges *n = SKdNodeEdges::Alloc(); + + // Compute the midpoints (just mean, though median would be better) of + // each component. + Vector ptAve = Vector::From(0, 0, 0); + SEdgeLl *flip; + int totaln = 0; + for(flip = sell; flip; flip = flip->next) { + ptAve = ptAve.Plus(flip->se->a); + ptAve = ptAve.Plus(flip->se->b); + totaln++; + } + ptAve = ptAve.ScaledBy(1.0 / (2*totaln)); + + // For each component, see how it splits. + int ltln[3] = { 0, 0, 0 }, gtln[3] = { 0, 0, 0 }; + double badness[3]; + for(flip = sell; flip; flip = flip->next) { + for(int i = 0; i < 3; i++) { + if(flip->se->a.Element(i) < ptAve.Element(i) + KDTREE_EPS || + flip->se->b.Element(i) < ptAve.Element(i) + KDTREE_EPS) + { + ltln[i]++; + } + if(flip->se->a.Element(i) > ptAve.Element(i) - KDTREE_EPS || + flip->se->b.Element(i) > ptAve.Element(i) - KDTREE_EPS) + { + gtln[i]++; + } + } + } + for(int i = 0; i < 3; i++) { + badness[i] = pow((double)ltln[i], 4) + pow((double)gtln[i], 4); + } + + // Choose the least bad coordinate to split along. + if(badness[0] < badness[1] && badness[0] < badness[2]) { + n->which = 0; + } else if(badness[1] < badness[2]) { + n->which = 1; + } else { + n->which = 2; + } + n->c = ptAve.Element(n->which); + + if(totaln < 3 || totaln == gtln[n->which] || totaln == ltln[n->which]) { + n->edges = sell; + // and we're a leaf node + return n; + } + + // Sort the edges according to which side(s) of the split plane they're on. + SEdgeLl *gtl = NULL, *ltl = NULL; + for(flip = sell; flip; flip = flip->next) { + if(flip->se->a.Element(n->which) < n->c + KDTREE_EPS || + flip->se->b.Element(n->which) < n->c + KDTREE_EPS) + { + SEdgeLl *selln = SEdgeLl::Alloc(); + selln->se = flip->se; + selln->next = ltl; + ltl = selln; + } + if(flip->se->a.Element(n->which) > n->c - KDTREE_EPS || + flip->se->b.Element(n->which) > n->c - KDTREE_EPS) + { + SEdgeLl *selln = SEdgeLl::Alloc(); + selln->se = flip->se; + selln->next = gtl; + gtl = selln; + } + } + + n->lt = SKdNodeEdges::From(ltl); + n->gt = SKdNodeEdges::From(gtl); + return n; +} + +int SKdNodeEdges::AnyEdgeCrossings(Vector a, Vector b, int cnt, + Vector *pi, SPointList *spl) const +{ + int inters = 0; + if(gt && lt) { + if(a.Element(which) < c + KDTREE_EPS || + b.Element(which) < c + KDTREE_EPS) + { + inters += lt->AnyEdgeCrossings(a, b, cnt, pi, spl); + } + if(a.Element(which) > c - KDTREE_EPS || + b.Element(which) > c - KDTREE_EPS) + { + inters += gt->AnyEdgeCrossings(a, b, cnt, pi, spl); + } + } else { + SEdgeLl *sell; + for(sell = edges; sell; sell = sell->next) { + SEdge *se = sell->se; + if(se->tag == cnt) continue; + if(se->EdgeCrosses(a, b, pi, spl)) { + inters++; + } + se->tag = cnt; + } + } + return inters; +} + +//----------------------------------------------------------------------------- +// We have an edge list that contains only collinear edges, maybe with more +// splits than necessary. Merge any collinear segments that join. +//----------------------------------------------------------------------------- +void SEdgeList::MergeCollinearSegments(Vector a, Vector b) { + const Vector lineStart = a; + const Vector lineDirection = b.Minus(a); + std::sort(l.begin(), l.end(), [&](const SEdge &a, const SEdge &b) { + double ta = (a.a.Minus(lineStart)).DivProjected(lineDirection); + double tb = (b.a.Minus(lineStart)).DivProjected(lineDirection); + + return (ta < tb); + }); + + l.ClearTags(); + SEdge *prev = nullptr; + for(auto &now : l) { + if(prev != nullptr) { + if((prev->b).Equals(now.a) && prev->auxA == now.auxA) { + // The previous segment joins up to us; so merge it into us. + prev->tag = 1; + now.a = prev->a; + } + } + prev = &now; + } + l.RemoveTagged(); +} + +void SPointList::Clear() { + l.Clear(); +} + +bool SPointList::ContainsPoint(Vector pt) const { + return (IndexForPoint(pt) >= 0); +} + +int SPointList::IndexForPoint(Vector pt) const { + int i; + for(i = 0; i < l.n; i++) { + const SPoint *p = &(l[i]); + if(pt.Equals(p->p)) { + return i; + } + } + // Not found, so return negative to indicate that. + return -1; +} + +void SPointList::IncrementTagFor(Vector pt) { + SPoint *p; + for(p = l.First(); p; p = l.NextAfter(p)) { + if(pt.Equals(p->p)) { + (p->tag)++; + return; + } + } + SPoint pa; + pa.p = pt; + pa.tag = 1; + l.Add(&pa); +} + +void SPointList::Add(Vector pt) { + SPoint p = {}; + p.p = pt; + l.Add(&p); +} + +void SContour::AddPoint(Vector p) { + SPoint sp; + sp.tag = 0; + sp.p = p; + + l.Add(&sp); +} + +void SContour::MakeEdgesInto(SEdgeList *el) const { + int i; + for(i = 0; i < (l.n - 1); i++) { + el->AddEdge(l[i].p, l[i+1].p); + } +} + +void SContour::CopyInto(SContour *dest) const { + for(const SPoint *sp = l.First(); sp; sp = l.NextAfter(sp)) { + dest->AddPoint(sp->p); + } +} + +void SContour::FindPointWithMinX() { + xminPt = Vector::From(1e10, 1e10, 1e10); + for(const SPoint *sp = l.First(); sp; sp = l.NextAfter(sp)) { + if(sp->p.x < xminPt.x) { + xminPt = sp->p; + } + } +} + +Vector SContour::ComputeNormal() const { + Vector n = Vector::From(0, 0, 0); + + for(int i = 0; i < l.n - 2; i++) { + Vector u = (l[i+1].p).Minus(l[i+0].p).WithMagnitude(1); + Vector v = (l[i+2].p).Minus(l[i+1].p).WithMagnitude(1); + Vector nt = u.Cross(v); + if(nt.Magnitude() > n.Magnitude()) { + n = nt; + } + } + return n.WithMagnitude(1); +} + +Vector SContour::AnyEdgeMidpoint() const { + ssassert(l.n >= 2, "Need two points to find a midpoint"); + return ((l[0].p).Plus(l[1].p)).ScaledBy(0.5); +} + +bool SContour::IsClockwiseProjdToNormal(Vector n) const { + // Degenerate things might happen as we draw; doesn't really matter + // what we do then. + if(n.Magnitude() < 0.01) return true; + + return (SignedAreaProjdToNormal(n) < 0); +} + +double SContour::SignedAreaProjdToNormal(Vector n) const { + // An arbitrary 2d coordinate system that has n as its normal + Vector u = n.Normal(0); + Vector v = n.Normal(1); + + double area = 0; + for(int i = 0; i < (l.n - 1); i++) { + double u0 = (l[i ].p).Dot(u); + double v0 = (l[i ].p).Dot(v); + double u1 = (l[i+1].p).Dot(u); + double v1 = (l[i+1].p).Dot(v); + + area += ((v0 + v1)/2)*(u1 - u0); + } + return area; +} + +bool SContour::ContainsPointProjdToNormal(Vector n, Vector p) const { + Vector u = n.Normal(0); + Vector v = n.Normal(1); + + double up = p.Dot(u); + double vp = p.Dot(v); + + bool inside = false; + for(int i = 0; i < (l.n - 1); i++) { + double ua = (l[i ].p).Dot(u); + double va = (l[i ].p).Dot(v); + // The curve needs to be exactly closed; approximation is death. + double ub = (l[(i+1)%(l.n-1)].p).Dot(u); + double vb = (l[(i+1)%(l.n-1)].p).Dot(v); + + if ((((va <= vp) && (vp < vb)) || + ((vb <= vp) && (vp < va))) && + (up < (ub - ua) * (vp - va) / (vb - va) + ua)) + { + inside = !inside; + } + } + + return inside; +} + +void SContour::Reverse() { + l.Reverse(); +} + + +void SPolygon::Clear() { + int i; + for(i = 0; i < l.n; i++) { + (l[i]).l.Clear(); + } + l.Clear(); +} + +void SPolygon::AddEmptyContour() { + SContour c = {}; + l.Add(&c); +} + +void SPolygon::MakeEdgesInto(SEdgeList *el) const { + int i; + for(i = 0; i < l.n; i++) { + (l[i]).MakeEdgesInto(el); + } +} + +Vector SPolygon::ComputeNormal() const { + if(l.IsEmpty()) + return Vector::From(0, 0, 0); + return (l[0]).ComputeNormal(); +} + +double SPolygon::SignedArea() const { + double area = 0; + // This returns the true area only if the contours are all oriented + // correctly, with the holes backwards from the outer contours. + for(const SContour *sc = l.First(); sc; sc = l.NextAfter(sc)) { + area += sc->SignedAreaProjdToNormal(normal); + } + return area; +} + +bool SPolygon::ContainsPoint(Vector p) const { + return (WindingNumberForPoint(p) % 2) == 1; +} + +size_t SPolygon::WindingNumberForPoint(Vector p) const { + auto winding = std::count_if(l.begin(), l.end(), [&](const SContour &sc) { + return sc.ContainsPointProjdToNormal(normal, p); + }); + return winding; +} + +void SPolygon::FixContourDirections() { + // At output, the contour's tag will be 1 if we reversed it, else 0. + l.ClearTags(); + + // Outside curve looks counterclockwise, projected against our normal. + int i, j; + for(i = 0; i < l.n; i++) { + SContour *sc = &(l[i]); + if(sc->l.n < 2) continue; + // The contours may not intersect, but they may share vertices; so + // testing a vertex for point-in-polygon may fail, but the midpoint + // of an edge is okay. + Vector pt = (((sc->l[0]).p).Plus(sc->l[1].p)).ScaledBy(0.5); + + sc->timesEnclosed = 0; + bool outer = true; + for(j = 0; j < l.n; j++) { + if(i == j) continue; + SContour *sct = &(l[j]); + if(sct->ContainsPointProjdToNormal(normal, pt)) { + outer = !outer; + (sc->timesEnclosed)++; + } + } + + bool clockwise = sc->IsClockwiseProjdToNormal(normal); + if((clockwise && outer) || (!clockwise && !outer)) { + sc->Reverse(); + sc->tag = 1; + } + } +} + +bool SPolygon::IsEmpty() const { + if(l.IsEmpty() || l[0].l.IsEmpty()) + return true; + return false; +} + +Vector SPolygon::AnyPoint() const { + ssassert(!IsEmpty(), "Need at least one point"); + return l[0].l[0].p; +} + +bool SPolygon::SelfIntersecting(Vector *intersectsAt) const { + SEdgeList el = {}; + MakeEdgesInto(&el); + SKdNodeEdges *kdtree = SKdNodeEdges::From(&el); + + int cnt = 1; + el.l.ClearTags(); + + bool ret = false; + SEdge *se; + for(se = el.l.First(); se; se = el.l.NextAfter(se)) { + int inters = kdtree->AnyEdgeCrossings(se->a, se->b, cnt, intersectsAt); + if(inters != 1) { + ret = true; + break; + } + cnt++; + } + + el.Clear(); + return ret; +} + +void SPolygon::InverseTransformInto(SPolygon *sp, Vector u, Vector v, Vector n) const { + for(const SContour &sc : l) { + SContour tsc = {}; + tsc.timesEnclosed = sc.timesEnclosed; + for(const SPoint &sp : sc.l) { + tsc.AddPoint(sp.p.DotInToCsys(u, v, n)); + } + sp->l.Add(&tsc); + } +} + +//----------------------------------------------------------------------------- +// Low-quality routines to cutter radius compensate a polygon. Assumes the +// polygon is in the xy plane, and the contours all go in the right direction +// with respect to normal (0, 0, -1). +//----------------------------------------------------------------------------- +void SPolygon::OffsetInto(SPolygon *dest, double r) const { + int i; + dest->Clear(); + for(i = 0; i < l.n; i++) { + const SContour *sc = &(l[i]); + dest->AddEmptyContour(); + sc->OffsetInto(&(dest->l[dest->l.n-1]), r); + } +} +//----------------------------------------------------------------------------- +// Calculate the intersection point of two lines. Returns true for success, +// false if they're parallel. +//----------------------------------------------------------------------------- +static bool IntersectionOfLines(double x0A, double y0A, double dxA, double dyA, + double x0B, double y0B, double dxB, double dyB, + double *xi, double *yi) +{ + double A[2][2]; + double b[2]; + + // The line is given to us in the form: + // (x(t), y(t)) = (x0, y0) + t*(dx, dy) + // so first rewrite it as + // (x - x0, y - y0) dot (dy, -dx) = 0 + // x*dy - x0*dy - y*dx + y0*dx = 0 + // x*dy - y*dx = (x0*dy - y0*dx) + + // So write the matrix, pre-pivoted. + if(fabs(dyA) > fabs(dyB)) { + A[0][0] = dyA; A[0][1] = -dxA; b[0] = x0A*dyA - y0A*dxA; + A[1][0] = dyB; A[1][1] = -dxB; b[1] = x0B*dyB - y0B*dxB; + } else { + A[1][0] = dyA; A[1][1] = -dxA; b[1] = x0A*dyA - y0A*dxA; + A[0][0] = dyB; A[0][1] = -dxB; b[0] = x0B*dyB - y0B*dxB; + } + + // Check the determinant; if it's zero then no solution. + if(fabs(A[0][0]*A[1][1] - A[0][1]*A[1][0]) < LENGTH_EPS) { + return false; + } + + // Solve + double v = A[1][0] / A[0][0]; + A[1][0] -= A[0][0]*v; + A[1][1] -= A[0][1]*v; + b[1] -= b[0]*v; + + // Back-substitute. + *yi = b[1] / A[1][1]; + *xi = (b[0] - A[0][1]*(*yi)) / A[0][0]; + + return true; +} +void SContour::OffsetInto(SContour *dest, double r) const { + int i; + + for(i = 0; i < l.n; i++) { + Vector a, b, c; + Vector dp, dn; + double thetan, thetap; + + a = l[WRAP(i-1, (l.n-1))].p; + b = l[WRAP(i, (l.n-1))].p; + c = l[WRAP(i+1, (l.n-1))].p; + + dp = a.Minus(b); + thetap = atan2(dp.y, dp.x); + + dn = b.Minus(c); + thetan = atan2(dn.y, dn.x); + + // A short line segment in a badly-generated polygon might look + // okay but screw up our sense of direction. + if(dp.Magnitude() < LENGTH_EPS || dn.Magnitude() < LENGTH_EPS) { + continue; + } + + if(thetan > thetap && (thetan - thetap) > PI) { + thetap += 2*PI; + } + if(thetan < thetap && (thetap - thetan) > PI) { + thetan += 2*PI; + } + + if(fabs(thetan - thetap) < (1*PI)/180) { + Vector p = { b.x - r*sin(thetap), b.y + r*cos(thetap), 0 }; + dest->AddPoint(p); + } else if(thetan < thetap) { + // This is an inside corner. We have two edges, Ep and En. Move + // out from their intersection by radius, normal to En, and + // then draw a line parallel to En. Move out from their + // intersection by radius, normal to Ep, and then draw a second + // line parallel to Ep. The point that we want to generate is + // the intersection of these two lines--it removes as much + // material as we can without removing any that we shouldn't. + double px0, py0, pdx, pdy; + double nx0, ny0, ndx, ndy; + double x = 0.0, y = 0.0; + + px0 = b.x - r*sin(thetap); + py0 = b.y + r*cos(thetap); + pdx = cos(thetap); + pdy = sin(thetap); + + nx0 = b.x - r*sin(thetan); + ny0 = b.y + r*cos(thetan); + ndx = cos(thetan); + ndy = sin(thetan); + + IntersectionOfLines(px0, py0, pdx, pdy, + nx0, ny0, ndx, ndy, + &x, &y); + + dest->AddPoint(Vector::From(x, y, 0)); + } else { + if(fabs(thetap - thetan) < (6*PI)/180) { + Vector pp = { b.x - r*sin(thetap), + b.y + r*cos(thetap), 0 }; + dest->AddPoint(pp); + + Vector pn = { b.x - r*sin(thetan), + b.y + r*cos(thetan), 0 }; + dest->AddPoint(pn); + } else { + double theta; + for(theta = thetap; theta <= thetan; theta += (6*PI)/180) { + Vector p = { b.x - r*sin(theta), + b.y + r*cos(theta), 0 }; + dest->AddPoint(p); + } + } + } + } +} + +} // namespace SolveSpace diff --git a/src/polygon.h b/src/polygon.h new file mode 100644 index 0000000..4668421 --- /dev/null +++ b/src/polygon.h @@ -0,0 +1,432 @@ +//----------------------------------------------------------------------------- +// Anything relating to plane polygons and triangles, and (generally, non- +// planar) meshes thereof. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- + +#ifndef SOLVESPACE_POLYGON_H +#define SOLVESPACE_POLYGON_H + +#include +#include +#include +#include + +#include "dsc.h" + +namespace SolveSpace { + +class Group; +class SPointList; +class SPolygon; +class SContour; +class SMesh; +class SSurface; +class SBsp3; +class SOutlineList; + +enum class EarType : uint32_t { + UNKNOWN = 0, + NOT_EAR = 1, + EAR = 2 +}; + +enum class BspClass : uint32_t { + POS = 100, + NEG = 101, + COPLANAR = 200 +}; + +enum class EdgeKind : uint32_t { + NAKED_OR_SELF_INTER = 100, + SELF_INTER = 200, + TURNING = 300, + EMPHASIZED = 400, + SHARP = 500, +}; + +class SEdge { +public: + int tag; + int auxA, auxB; + Vector a, b; + + static SEdge From(Vector a, Vector b); + bool EdgeCrosses(Vector a, Vector b, Vector *pi=NULL, SPointList *spl=NULL) const; +}; + +class SEdgeList { +public: + List l; + + void Clear(); + void AddEdge(Vector a, Vector b, int auxA=0, int auxB=0, int tag=0); + bool AssemblePolygon(SPolygon *dest, SEdge *errorAt, bool keepDir=false) const; + bool AssembleContour(Vector first, Vector last, SContour *dest, + SEdge *errorAt, bool keepDir, int start) const; + int AnyEdgeCrossings(Vector a, Vector b, + Vector *pi=NULL, SPointList *spl=NULL) const; + bool ContainsEdgeFrom(const SEdgeList *sel) const; + bool ContainsEdge(const SEdge *se) const; + void CullExtraneousEdges(bool both=true); + void MergeCollinearSegments(Vector a, Vector b); +}; + +// A kd-tree element needs to go on a side of a node if it's when KDTREE_EPS +// of the boundary. So increasing this number never breaks anything, but may +// result in more duplicated elements. So it's conservative to be sloppy here. +#define KDTREE_EPS (20*LENGTH_EPS) + +class SEdgeLl { +public: + SEdge *se; + SEdgeLl *next; + + static SEdgeLl *Alloc(); +}; + +class SKdNodeEdges { +public: + int which; // whether c is x, y, or z + double c; + SKdNodeEdges *gt; + SKdNodeEdges *lt; + + SEdgeLl *edges; + + static SKdNodeEdges *From(SEdgeList *sel); + static SKdNodeEdges *From(SEdgeLl *sell); + static SKdNodeEdges *Alloc(); + int AnyEdgeCrossings(Vector a, Vector b, int cnt, + Vector *pi=NULL, SPointList *spl=NULL) const; +}; + +class SPoint { +public: + int tag; + + EarType ear; + + Vector p; + Vector auxv; +}; + +class SPointList { +public: + List l; + + void Clear(); + bool ContainsPoint(Vector pt) const; + int IndexForPoint(Vector pt) const; + void IncrementTagFor(Vector pt); + void Add(Vector pt); +}; + +class SContour { +public: + int tag; + int timesEnclosed; + Vector xminPt; + List l; + + void AddPoint(Vector p); + void MakeEdgesInto(SEdgeList *el) const; + void Reverse(); + Vector ComputeNormal() const; + double SignedAreaProjdToNormal(Vector n) const; + bool IsClockwiseProjdToNormal(Vector n) const; + bool ContainsPointProjdToNormal(Vector n, Vector p) const; + void OffsetInto(SContour *dest, double r) const; + void CopyInto(SContour *dest) const; + void FindPointWithMinX(); + Vector AnyEdgeMidpoint() const; + + bool IsEmptyTriangle(int ap, int bp, int cp, double scaledEPS) const; + bool IsEar(int bp, double scaledEps) const; + bool BridgeToContour(SContour *sc, SEdgeList *el, List *vl); + void ClipEarInto(SMesh *m, int bp, double scaledEps); + void UvTriangulateInto(SMesh *m, SSurface *srf); +}; + +typedef struct { + uint32_t face; + RgbaColor color; +} STriMeta; + +class SPolygon { +public: + List l; + Vector normal; + + Vector ComputeNormal() const; + void AddEmptyContour(); + size_t WindingNumberForPoint(Vector p) const; + double SignedArea() const; + bool ContainsPoint(Vector p) const; + void MakeEdgesInto(SEdgeList *el) const; + void FixContourDirections(); + void Clear(); + bool SelfIntersecting(Vector *intersectsAt) const; + bool IsEmpty() const; + Vector AnyPoint() const; + void OffsetInto(SPolygon *dest, double r) const; + void UvTriangulateInto(SMesh *m, SSurface *srf); + void UvGridTriangulateInto(SMesh *m, SSurface *srf); + void TriangulateInto(SMesh *m) const; + void InverseTransformInto(SPolygon *sp, Vector u, Vector v, Vector n) const; +}; + +class STriangle { +public: + int tag; + STriMeta meta; + + union { + struct { Vector a, b, c; }; + Vector vertices[3]; + }; + + union { + struct { Vector an, bn, cn; }; + Vector normals[3]; + }; + + static STriangle From(STriMeta meta, Vector a, Vector b, Vector c); + Vector Normal() const; + void FlipNormal(); + double MinAltitude() const; + bool ContainsPoint(Vector p) const; + bool ContainsPointProjd(Vector n, Vector p) const; + STriangle Transform(Vector o, Vector u, Vector v) const; + bool Raytrace(const Vector &rayPoint, const Vector &rayDir, + double *t, Vector *inters) const; + double SignedVolume() const; + double Area() const; + bool IsDegenerate() const; +}; + +class SBsp2 { +public: + Vector np; // normal to the plane + + Vector no; // outer normal to the edge + double d; + SEdge edge; + + SBsp2 *pos; + SBsp2 *neg; + + SBsp2 *more; + + void InsertTriangleHow(BspClass how, STriangle *tr, SMesh *m, SBsp3 *bsp3); + void InsertTriangle(STriangle *tr, SMesh *m, SBsp3 *bsp3); + Vector IntersectionWith(Vector a, Vector b) const; + void InsertEdge(SEdge *nedge, Vector nnp, Vector out); + static SBsp2 *InsertOrCreateEdge(SBsp2 *where, SEdge *nedge, + Vector nnp, Vector out); + static SBsp2 *Alloc(); +}; + +class SBsp3 { +public: + Vector n; + double d; + + STriangle tri; + SBsp3 *pos; + SBsp3 *neg; + + SBsp3 *more; + + SBsp2 *edges; + + static SBsp3 *Alloc(); + static SBsp3 *FromMesh(const SMesh *m); + + Vector IntersectionWith(Vector a, Vector b) const; + + void InsertHow(BspClass how, STriangle *str, SMesh *instead); + void Insert(STriangle *str, SMesh *instead); + static SBsp3 *InsertOrCreate(SBsp3 *where, STriangle *str, SMesh *instead); + + void InsertConvexHow(BspClass how, STriMeta meta, Vector *vertex, size_t n, + SMesh *instead); + SBsp3 *InsertConvex(STriMeta meta, Vector *vertex, size_t n, SMesh *instead); + + void InsertInPlane(bool pos2, STriangle *tr, SMesh *m); + + void GenerateInPaintOrder(SMesh *m) const; +}; + +class SMesh { +public: + List l; + + bool flipNormal; + bool keepInsideOtherShell; + bool keepCoplanar; + bool atLeastOneDiscarded; + bool isTransparent; + + void Clear(); + void AddTriangle(const STriangle *st); + void AddTriangle(STriMeta meta, Vector a, Vector b, Vector c); + void AddTriangle(STriMeta meta, Vector n, + Vector a, Vector b, Vector c); + void DoBounding(Vector v, Vector *vmax, Vector *vmin) const; + void GetBounding(Vector *vmax, Vector *vmin) const; + + void Simplify(int start); + + void AddAgainstBsp(SMesh *srcm, SBsp3 *bsp3); + void MakeFromUnionOf(SMesh *a, SMesh *b); + void MakeFromDifferenceOf(SMesh *a, SMesh *b); + void MakeFromIntersectionOf(SMesh *a, SMesh *b); + + void MakeFromCopyOf(SMesh *a); + void MakeFromTransformationOf(SMesh *a, Vector trans, + Quaternion q, double scale); + void MakeFromAssemblyOf(SMesh *a, SMesh *b); + + void MakeEdgesInPlaneInto(SEdgeList *sel, Vector n, double d); + void MakeOutlinesInto(SOutlineList *sol, EdgeKind type); + + void PrecomputeTransparency(); + void RemoveDegenerateTriangles(); + double CalculateVolume() const; + double CalculateSurfaceArea(const std::vector &faces) const; + + bool IsEmpty() const; + void RemapFaces(Group *g, int remap); + + uint32_t FirstIntersectionWith(Point2d mp) const; + + Vector GetCenterOfMass() const; +}; + +// A linked list of triangles +class STriangleLl { +public: + STriangle *tri; + + STriangleLl *next; + + static STriangleLl *Alloc(); +}; + +class SOutline { +public: + int tag; + Vector a, b, nl, nr; + + bool IsVisible(Vector projDir) const; +}; + +class SOutlineList { +public: + List l; + + void Clear(); + void AddEdge(Vector a, Vector b, Vector nl, Vector nr, int tag = 0); + void ListTaggedInto(SEdgeList *el, int auxA = 0, int auxB = 0); + + void MakeFromCopyOf(SOutlineList *ol); +}; + +class SKdNode { +public: + struct EdgeOnInfo { + int count; + bool frontFacing; + bool intersectsMesh; + STriangle *tr; + int ai; + int bi; + }; + + int which; // whether c is x, y, or z + double c; + + SKdNode *gt; + SKdNode *lt; + + STriangleLl *tris; + + static SKdNode *Alloc(); + static SKdNode *From(SMesh *m); + static SKdNode *From(STriangleLl *tll); + + void AddTriangle(STriangle *tr); + void MakeMeshInto(SMesh *m) const; + void ListTrianglesInto(std::vector *tl) const; + void ClearTags() const; + + void FindEdgeOn(Vector a, Vector b, int cnt, bool coplanarIsInter, EdgeOnInfo *info) const; + void MakeCertainEdgesInto(SEdgeList *sel, EdgeKind how, bool coplanarIsInter, + bool *inter, bool *leaky, int auxA = 0) const; + void MakeOutlinesInto(SOutlineList *sel, EdgeKind tagKind) const; + + void OcclusionTestLine(SEdge orig, SEdgeList *sel, int cnt) const; + void SplitLinesAgainstTriangle(SEdgeList *sel, STriangle *tr) const; + + void SnapToMesh(SMesh *m); + void SnapToVertex(Vector v, SMesh *extras); +}; + +class PolylineBuilder { +public: + struct Edge; + + struct Vertex { + Vector pos; + std::vector edges; + + bool GetNext(uint32_t kind, Vertex **next, Edge **nextEdge); + bool GetNext(uint32_t kind, Vector plane, double d, Vertex **next, Edge **nextEdge); + size_t CountEdgesWithTagAndKind(int tag, uint32_t kind) const; + }; + + struct VertexPairHash { + size_t operator()(const std::pair &v) const; + }; + + struct Edge { + Vertex *a; + Vertex *b; + uint32_t kind; + int tag; + + union { + uintptr_t data; + SOutline *outline; + SEdge *edge; + }; + + Vertex *GetOtherVertex(Vertex *v) const; + bool GetStartAndNext(Vertex **start, Vertex **next, bool loop) const; + }; + + std::unordered_map vertices; + std::unordered_map, Edge *, VertexPairHash> edgeMap; + std::vector edges; + + ~PolylineBuilder(); + void Clear(); + + Vertex *AddVertex(const Vector &pos); + Edge *AddEdge(const Vector &p0, const Vector &p1, uint32_t kind, uintptr_t data = 0); + void Generate(std::function const &startFunc, + std::function const &nextFunc, + std::function const &aloneFunc, + std::function const &endFunc = []() {}); + + void MakeFromEdges(const SEdgeList &sel); + void MakeFromOutlines(const SOutlineList &sol); + void GenerateEdges(SEdgeList *sel); + void GenerateOutlines(SOutlineList *sol); +}; + +} // namespace SolveSpace + +#endif diff --git a/src/polyline.cpp b/src/polyline.cpp new file mode 100644 index 0000000..0b7e2e9 --- /dev/null +++ b/src/polyline.cpp @@ -0,0 +1,247 @@ +//----------------------------------------------------------------------------- +// A helper class to assemble scattered edges into contiguous polylines, +// as nicely as possible. +// +// Copyright 2016 M-Labs Ltd +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +bool PolylineBuilder::Vertex::GetNext(uint32_t kind, Vertex **next, Edge **nextEdge) { + auto it = std::find_if(edges.begin(), edges.end(), [&](const Edge *e) { + return e->tag == 0 && e->kind == kind; + }); + + if(it != edges.end()) { + (*it)->tag = 1; + *next = (*it)->GetOtherVertex(this); + *nextEdge = *it; + return true; + } + + return false; +} + +bool PolylineBuilder::Vertex::GetNext(uint32_t kind, Vector plane, double dist, + Vertex **next, Edge **nextEdge) { + Edge *best = NULL; + double minD = VERY_POSITIVE; + for(Edge *e : edges) { + if(e->tag != 0) continue; + if(e->kind != kind) continue; + + // We choose the best next edge with minimal distance from the current plane + Vector nextPos = e->GetOtherVertex(this)->pos; + double curD = fabs(plane.Dot(nextPos) - dist); + if(best != NULL && curD > minD) continue; + best = e; + minD = curD; + } + + if(best != NULL) { + best->tag = 1; + *next = best->GetOtherVertex(this); + *nextEdge = best; + return true; + } + + return false; +} + + +size_t PolylineBuilder::Vertex::CountEdgesWithTagAndKind(int tag, uint32_t kind) const { + return std::count_if(edges.begin(), edges.end(), [&](const Edge *e) { + return e->tag == tag && e->kind == kind; + }); +} + +PolylineBuilder::Vertex *PolylineBuilder::Edge::GetOtherVertex(PolylineBuilder::Vertex *v) const { + if(a == v) return b; + if(b == v) return a; + return NULL; +} + +size_t PolylineBuilder::VertexPairHash::operator()(const std::pair &v) const { + return ((uintptr_t)v.first / sizeof(Vertex)) ^ + ((uintptr_t)v.second / sizeof(Vertex)); +} + +bool PolylineBuilder::Edge::GetStartAndNext(PolylineBuilder::Vertex **start, + PolylineBuilder::Vertex **next, bool loop) const { + size_t numA = a->CountEdgesWithTagAndKind(0, kind); + size_t numB = b->CountEdgesWithTagAndKind(0, kind); + + if((numA == 1 && numB > 1) || (loop && numA > 1 && numB > 1)) { + *start = a; + *next = b; + return true; + } + + if(numA > 1 && numB == 1) { + *start = b; + *next = a; + return true; + } + + return false; +} + +PolylineBuilder::~PolylineBuilder() { + Clear(); +} + +void PolylineBuilder::Clear() { + for(Edge *e : edges) { + delete e; + } + edges.clear(); + + for(auto &v : vertices) { + delete v.second; + } + vertices.clear(); +} + +PolylineBuilder::Vertex *PolylineBuilder::AddVertex(const Vector &pos) { + auto it = vertices.find(pos); + if(it != vertices.end()) { + return it->second; + } + + Vertex *result = new Vertex; + result->pos = pos; + vertices.emplace(pos, result); + + return result; +} + +PolylineBuilder::Edge *PolylineBuilder::AddEdge(const Vector &p0, const Vector &p1, + uint32_t kind, uintptr_t data) { + Vertex *v0 = AddVertex(p0); + Vertex *v1 = AddVertex(p1); + if(v0 == v1) return NULL; + + auto it = edgeMap.find(std::make_pair(v0, v1)); + if(it != edgeMap.end()) { + return it->second; + } + + PolylineBuilder::Edge *edge = new PolylineBuilder::Edge {}; + edge->a = v0; + edge->b = v1; + edge->kind = kind; + edge->tag = 0; + edge->data = data; + edges.push_back(edge); + edgeMap.emplace(std::make_pair(v0, v1), edge); + + v0->edges.push_back(edge); + v1->edges.push_back(edge); + + return edge; +} + +void PolylineBuilder::Generate( + std::function const &startFunc, + std::function const &nextFunc, + std::function const &aloneFunc, std::function const &endFunc) { + bool found; + bool loop = false; + do { + found = false; + for(PolylineBuilder::Edge *e : edges) { + if(e->tag != 0) continue; + + Vertex *start; + Vertex *next; + if(!e->GetStartAndNext(&start, &next, loop)) continue; + + Vector startPos = start->pos; + Vector nextPos = next->pos; + found = true; + e->tag = 1; + + startFunc(start, next, e); + + Edge *nextEdge; + if(next->GetNext(e->kind, &next, &nextEdge)) { + Vector plane = nextPos.Minus(startPos).Cross(next->pos.Minus(startPos)); + double dist = plane.Dot(startPos); + nextFunc(next, nextEdge); + while(next->GetNext(e->kind, plane, dist, &next, &nextEdge)) { + nextFunc(next, nextEdge); + } + } + + endFunc(); + } + + if(!found && !loop) { + loop = true; + found = true; + } + } while(found); + + for(PolylineBuilder::Edge *e : edges) { + if(e->tag != 0) continue; + aloneFunc(e); + } +} + +void PolylineBuilder::MakeFromEdges(const SEdgeList &sel) { + for(const SEdge &se : sel.l) { + AddEdge(se.a, se.b, (uint32_t)se.auxA, reinterpret_cast(&se)); + } +} + +void PolylineBuilder::MakeFromOutlines(const SOutlineList &ol) { + for(const SOutline &so : ol.l) { + // Use outline tag as kind, so that emphasized and contour outlines + // would not be composed together. + AddEdge(so.a, so.b, (uint32_t)so.tag, reinterpret_cast(&so)); + } +} + +void PolylineBuilder::GenerateEdges(SEdgeList *sel) { + Vector prev; + auto startFunc = [&](Vertex *start, Vertex *next, Edge *e) { + sel->AddEdge(start->pos, next->pos, e->kind); + prev = next->pos; + }; + + auto nextFunc = [&](Vertex *next, Edge *e) { + sel->AddEdge(prev, next->pos, e->kind); + prev = next->pos; + }; + + auto aloneFunc = [&](Edge *e) { + sel->AddEdge(e->a->pos, e->b->pos, e->kind); + }; + + Generate(startFunc, nextFunc, aloneFunc); +} + +void PolylineBuilder::GenerateOutlines(SOutlineList *sol) { + Vector prev; + auto startFunc = [&](Vertex *start, Vertex *next, Edge *e) { + SOutline *so = e->outline; + sol->AddEdge(start->pos, next->pos, so->nl, so->nr, so->tag); + prev = next->pos; + }; + + auto nextFunc = [&](Vertex *next, Edge *e) { + SOutline *so = e->outline; + sol->AddEdge(prev, next->pos, so->nl, so->nr, so->tag); + prev = next->pos; + }; + + auto aloneFunc = [&](Edge *e) { + SOutline *so = e->outline; + sol->AddEdge(so->a, so->b, so->nl, so->nr, so->tag); + }; + + Generate(startFunc, nextFunc, aloneFunc); +} + +} // namespace SolveSpace diff --git a/src/render/gl3shader.cpp b/src/render/gl3shader.cpp new file mode 100644 index 0000000..7c4552e --- /dev/null +++ b/src/render/gl3shader.cpp @@ -0,0 +1,1071 @@ +//----------------------------------------------------------------------------- +// OpenGL ES 2.0 and OpenGL 3.0 shader interface. +// +// Copyright 2016 Aleksey Egorov +//----------------------------------------------------------------------------- +#include "solvespace.h" +#include "gl3shader.h" + +namespace SolveSpace { + +//----------------------------------------------------------------------------- +// Floating point data structures +//----------------------------------------------------------------------------- + +Vector2f Vector2f::From(float x, float y) { + return { x, y }; +} + +Vector2f Vector2f::From(double x, double y) { + return { (float)x, (float)y }; +} + +Vector2f Vector2f::FromInt(uint32_t x, uint32_t y) { + return { (float)x, (float)y }; +} + +Vector3f Vector3f::From(float x, float y, float z) { + return { x, y, z }; +} + +Vector3f Vector3f::From(const Vector &v) { + return { (float)v.x, (float)v.y, (float)v.z }; +} + +Vector3f Vector3f::From(const RgbaColor &c) { + return { c.redF(), c.greenF(), c.blueF() }; +} + +Vector4f Vector4f::From(float x, float y, float z, float w) { + return { x, y, z, w }; +} + +Vector4f Vector4f::From(const Vector &v, float w) { + return { (float)v.x, (float)v.y, (float)v.z, w }; +} + +Vector4f Vector4f::FromInt(uint32_t x, uint32_t y, uint32_t z, uint32_t w) { + return { (float)x, (float)y, (float)z, (float)w }; +} + +Vector4f Vector4f::From(const RgbaColor &c) { + return { c.redF(), c.greenF(), c.blueF(), c.alphaF() }; +} + +//----------------------------------------------------------------------------- +// Shader manipulation +//----------------------------------------------------------------------------- + +static GLuint CompileShader(const std::string &res, GLenum type) { + size_t size; + const char *resData = (const char *)Platform::LoadResource(res, &size); + + // Sigh, here we go... We want to deploy to four platforms: Linux, Windows, OS X, mobile+web. + // These platforms are basically disjunctive in the OpenGL versions and profiles that they + // support: mobile+web support GLES2, Windows can only be guaranteed to support GL1 without + // vendor's drivers installed but supports D3D9+ natively, Linux supports GL3.2+ and/or + // GLES2+ depending on whether we run on X11 or Wayland, and OS X supports either a legacy + // profile or a GL3.2 core profile or (on 10.9+) a GL4.1 core profile. + // The platforms barely have a common subset of features: + // * mobile+web and Windows (D3D9 through ANGLE) are strictly GLES2/GLSL1.0; + // * OS X legacy compatibility profile has GLSL1.2 only shaders, and GL3.2 core profile + // that has GLSL1.0 shaders compatible with GLES2 makes mandatory the use of vertex array + // objects, which cannot be used in GLES2 at all; similarly GL3.2 core has GL_RED but not + // GL_ALPHA whereas GLES2 has GL_ALPHA but not GL_RED. + // * GTK does not work on anything prior to GL3.0/GLES2.0; it does not permit explicitly + // asking for a compatibility profile, i.e. you can only ask for 3.2+; and it does not + // permit asking for a GLES profile prior to GTK 3.22, which will get into Ubuntu + // no earlier than late 2017. This is despite the fact that if only GTK defaulted + // to the compatibility profile, everything would have just worked as Mesa is + // very permissive. + // While we're at it, let's remember that GLES2 has *only* glDepthRangef, GL3.2 has *only* + // glDepthRange, and GL4.1+ has both glDepthRangef and glDepthRange. Also, that GLSL1.0 + // makes `precision highp float;` mandatory in fragment shaders, and GLSL1.2 removes + // the `precision` keyword entirely, because that's clearly how minor versions work. + // Christ, what a trash fire. + + const char *prelude; +#if defined(HAVE_GLES) + prelude = R"( +#version 100 +#define TEX_ALPHA a +precision highp float; +)"; +#else + prelude = R"( +#version 120 +#define TEX_ALPHA r +)"; +#endif + std::string src(resData, size); + src = prelude + src; + + GLuint shader = glCreateShader(type); + ssassert(shader != 0, "glCreateShader failed"); + + const GLint glSize[] = { (int)src.length() }; + const GLchar* glSource[] = { src.c_str() }; + glShaderSource(shader, 1, glSource, glSize); + glCompileShader(shader); + + GLint infoLen = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen); + if(infoLen > 1) { + std::string infoStr(infoLen, '\0'); + glGetShaderInfoLog(shader, infoLen, NULL, &infoStr[0]); + dbp(infoStr.c_str()); + } + + GLint compiled = 0; + glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + if(!compiled) { + dbp("Failed to compile shader:\n" + "----8<----8<----8<----8<----8<----\n" + "%s\n" + "----8<----8<----8<----8<----8<----\n", + src.c_str()); + } + ssassert(compiled, "Cannot compile shader"); + + return shader; +} + +void Shader::Init(const std::string &vertexRes, const std::string &fragmentRes, + const std::vector > &locations) { + GLuint vert = CompileShader(vertexRes, GL_VERTEX_SHADER); + GLuint frag = CompileShader(fragmentRes, GL_FRAGMENT_SHADER); + + program = glCreateProgram(); + ssassert(program != 0, "glCreateProgram failed"); + + glAttachShader(program, vert); + glAttachShader(program, frag); + for(const auto &l : locations) { + glBindAttribLocation(program, l.first, l.second.c_str()); + } + glLinkProgram(program); + + GLint infoLen = 0; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLen); + if(infoLen > 1) { + std::string infoStr(infoLen, '\0'); + glGetProgramInfoLog(program, infoLen, NULL, &infoStr[0]); + dbp(infoStr.c_str()); + } + + GLint linked = 0; + glGetProgramiv(program, GL_LINK_STATUS, &linked); + ssassert(linked, "Cannot link shader"); +} + +void Shader::Clear() { + glDeleteProgram(program); +} + +void Shader::SetUniformMatrix(const char *name, const double *md) { + Enable(); + float mf[16]; + for(int i = 0; i < 16; i++) mf[i] = (float)md[i]; + glUniformMatrix4fv(glGetUniformLocation(program, name), 1, false, mf); +} + +void Shader::SetUniformVector(const char *name, const Vector &v) { + Enable(); + glUniform3f(glGetUniformLocation(program, name), (float)v.x, (float)v.y, (float)v.z); +} + +void Shader::SetUniformVector(const char *name, const Vector4f &v) { + Enable(); + glUniform4f(glGetUniformLocation(program, name), v.x, v.y, v.z, v.w); +} + +void Shader::SetUniformColor(const char *name, RgbaColor c) { + Enable(); + glUniform4f(glGetUniformLocation(program, name), c.redF(), c.greenF(), c.blueF(), c.alphaF()); +} + +void Shader::SetUniformFloat(const char *name, float v) { + Enable(); + glUniform1f(glGetUniformLocation(program, name), v); +} + +void Shader::SetUniformInt(const char *name, GLint v) { + Enable(); + glUniform1i(glGetUniformLocation(program, name), v); +} + +void Shader::SetUniformTextureUnit(const char *name, GLint index) { + Enable(); + glUniform1i(glGetUniformLocation(program, name), index); +} + +void Shader::Enable() const { + glUseProgram(program); +} + +void Shader::Disable() const { + glUseProgram(0); +} + +//----------------------------------------------------------------------------- +// Mesh rendering +//----------------------------------------------------------------------------- + +void MeshRenderer::Init() { + lightShader.Init( + "shaders/mesh.vert", "shaders/mesh.frag", + { + { ATTRIB_POS, "pos" }, + { ATTRIB_NOR, "nor" }, + { ATTRIB_COL, "col" }, + } + ); + + fillShader.Init( + "shaders/mesh_fill.vert", "shaders/mesh_fill.frag", + { + { ATTRIB_POS, "pos" }, + } + ); + fillShader.SetUniformTextureUnit("texture_", 0); + + selectedShader = &lightShader; +} + +void MeshRenderer::Clear() { + lightShader.Clear(); + fillShader.Clear(); +} + +MeshRenderer::Handle MeshRenderer::Add(const SMesh &m, bool dynamic) { + Handle handle; + glGenBuffers(1, &handle.vertexBuffer); + glBindBuffer(GL_ARRAY_BUFFER, handle.vertexBuffer); + + MeshVertex *vertices = new MeshVertex[m.l.n * 3]; + for(int i = 0; i < m.l.n; i++) { + const STriangle &t = m.l[i]; + vertices[i * 3 + 0].pos = Vector3f::From(t.a); + vertices[i * 3 + 1].pos = Vector3f::From(t.b); + vertices[i * 3 + 2].pos = Vector3f::From(t.c); + + if(t.an.EqualsExactly(Vector::From(0, 0, 0))) { + Vector3f normal = Vector3f::From(t.Normal()); + vertices[i * 3 + 0].nor = normal; + vertices[i * 3 + 1].nor = normal; + vertices[i * 3 + 2].nor = normal; + } else { + vertices[i * 3 + 0].nor = Vector3f::From(t.an); + vertices[i * 3 + 1].nor = Vector3f::From(t.bn); + vertices[i * 3 + 2].nor = Vector3f::From(t.cn); + } + + for(int j = 0; j < 3; j++) { + vertices[i * 3 + j].col = Vector4f::From(t.meta.color); + } + + } + glBufferData(GL_ARRAY_BUFFER, m.l.n * 3 * sizeof(MeshVertex), + vertices, dynamic ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW); + handle.size = m.l.n * 3; + delete []vertices; + + return handle; +} + +void MeshRenderer::Remove(const MeshRenderer::Handle &handle) { + glDeleteBuffers(1, &handle.vertexBuffer); +} + +void MeshRenderer::Draw(const MeshRenderer::Handle &handle, + bool useColors, RgbaColor overrideColor) { + if(handle.size == 0) return; + + selectedShader->Enable(); + + glBindBuffer(GL_ARRAY_BUFFER, handle.vertexBuffer); + + glEnableVertexAttribArray(ATTRIB_POS); + glVertexAttribPointer(ATTRIB_POS, 3, GL_FLOAT, GL_FALSE, sizeof(MeshVertex), + (void *)offsetof(MeshVertex, pos)); + + if(selectedShader == &lightShader) { + glEnableVertexAttribArray(ATTRIB_NOR); + glVertexAttribPointer(ATTRIB_NOR, 3, GL_FLOAT, GL_FALSE, sizeof(MeshVertex), + (void *)offsetof(MeshVertex, nor)); + if(useColors) { + glEnableVertexAttribArray(ATTRIB_COL); + glVertexAttribPointer(ATTRIB_COL, 4, GL_FLOAT, GL_FALSE, sizeof(MeshVertex), + (void *)offsetof(MeshVertex, col)); + } else { + glVertexAttrib4f(ATTRIB_COL, overrideColor.redF(), overrideColor.greenF(), overrideColor.blueF(), overrideColor.alphaF()); + } + } + + glDrawArrays(GL_TRIANGLES, 0, handle.size); + + glDisableVertexAttribArray(ATTRIB_POS); + if(selectedShader == &lightShader) { + glDisableVertexAttribArray(ATTRIB_NOR); + if(useColors) glDisableVertexAttribArray(ATTRIB_COL); + } + + glBindBuffer(GL_ARRAY_BUFFER, 0); + + selectedShader->Disable(); +} + +void MeshRenderer::Draw(const SMesh &mesh, bool useColors, RgbaColor overrideColor) { + Handle handle = Add(mesh, /*dynamic=*/true); + Draw(handle, useColors, overrideColor); + Remove(handle); +} + +void MeshRenderer::SetModelview(double *matrix) { + lightShader.SetUniformMatrix("modelview", matrix); + fillShader.SetUniformMatrix("modelview", matrix); +} + +void MeshRenderer::SetProjection(double *matrix) { + lightShader.SetUniformMatrix("projection", matrix); + fillShader.SetUniformMatrix("projection", matrix); +} + +void MeshRenderer::UseShaded(const Lighting &lighting) { + Vector dir0 = lighting.lightDirection[0]; + Vector dir1 = lighting.lightDirection[1]; + dir0.z = -dir0.z; + dir1.z = -dir1.z; + + lightShader.SetUniformVector("lightDir0", dir0); + lightShader.SetUniformFloat("lightInt0", (float)lighting.lightIntensity[0]); + lightShader.SetUniformVector("lightDir1", dir1); + lightShader.SetUniformFloat("lightInt1", (float)lighting.lightIntensity[1]); + lightShader.SetUniformFloat("ambient", (float)lighting.ambientIntensity); + selectedShader = &lightShader; +} + +void MeshRenderer::UseFilled(const Canvas::Fill &fill) { + fillShader.SetUniformColor("color", fill.color); + selectedShader = &fillShader; +} + +//----------------------------------------------------------------------------- +// Arrangement of stipple patterns into textures +//----------------------------------------------------------------------------- + +static double Frac(double x) { + return x - floor(x); +} + +static RgbaColor EncodeLengthAsFloat(double v) { + v = max(0.0, min(1.0, v)); + double er = v; + double eg = Frac(255.0 * v); + double eb = Frac(65025.0 * v); + double ea = Frac(160581375.0 * v); + + double r = er - eg / 255.0; + double g = eg - eb / 255.0; + double b = eb - ea / 255.0; + return RgbaColor::From((int)floor( r * 255.0 + 0.5), + (int)floor( g * 255.0 + 0.5), + (int)floor( b * 255.0 + 0.5), + (int)floor(ea * 255.0 + 0.5)); +} + +GLuint Generate(const std::vector &pattern) { + double patternLen = 0.0; + for(double s : pattern) { + patternLen += s; + } + + GLuint texture; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + + GLint size = 0; + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &size); + size /= 2; + + RgbaColor *textureData = new RgbaColor[size]; + int mipCount = (int)log2(size) + 1; + for(int mip = 0; mip < mipCount; mip++) { + int dashI = 0; + double dashT = 0.0; + for(int i = 0; i < size; i++) { + if(pattern.empty()) { + textureData[i] = EncodeLengthAsFloat(0.0); + continue; + } + + double t = (double)i / (double)(size - 1); + while(t - LENGTH_EPS > dashT + pattern[dashI] / patternLen) { + dashT += pattern[dashI] / patternLen; + dashI++; + } + double dashW = pattern[dashI] / patternLen; + if(dashI % 2 == 0) { + textureData[i] = EncodeLengthAsFloat(0.0); + } else { + double value; + if(t - dashT < pattern[dashI] / patternLen / 2.0) { + value = t - dashT; + } else { + value = dashT + dashW - t; + } + value = value * patternLen; + textureData[i] = EncodeLengthAsFloat(value); + } + } + glTexImage2D(GL_TEXTURE_2D, mip, GL_RGBA, size, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, + textureData); + size /= 2; + } + delete []textureData; + + return texture; +} + +void StippleAtlas::Init() { + for(uint32_t i = 0; i <= (uint32_t)StipplePattern::LAST; i++) { + patterns.push_back(Generate(StipplePatternDashes((StipplePattern)i))); + } +} + +void StippleAtlas::Clear() { + for(GLuint p : patterns) { + glDeleteTextures(1, &p); + } +} + +GLint StippleAtlas::GetTexture(StipplePattern pattern) const { + return patterns[(uint32_t)pattern]; +} + +double StippleAtlas::GetLength(StipplePattern pattern) const { + if(pattern == StipplePattern::CONTINUOUS) { + return 1.0; + } + return StipplePatternLength(pattern); +} + +//----------------------------------------------------------------------------- +// Edge rendering +//----------------------------------------------------------------------------- + +void EdgeRenderer::Init(const StippleAtlas *a) { + atlas = a; + shader.Init( + "shaders/edge.vert", "shaders/edge.frag", + { + { ATTRIB_POS, "pos" }, + { ATTRIB_LOC, "loc" }, + { ATTRIB_TAN, "tgt" } + } + ); +} + +void EdgeRenderer::Clear() { + shader.Clear(); +} + +EdgeRenderer::Handle EdgeRenderer::Add(const SEdgeList &edges, bool dynamic) { + Handle handle; + glGenBuffers(1, &handle.vertexBuffer); + glGenBuffers(1, &handle.indexBuffer); + + glBindBuffer(GL_ARRAY_BUFFER, handle.vertexBuffer); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, handle.indexBuffer); + + EdgeVertex *vertices = new EdgeVertex[edges.l.n * 8]; + uint32_t *indices = new uint32_t[edges.l.n * 6 * 3]; + double phase = 0.0; + uint32_t curVertex = 0; + uint32_t curIndex = 0; + for(int i = 0; i < edges.l.n; i++) { + const SEdge &curr = edges.l[i]; + const SEdge &next = edges.l[(i + 1) % edges.l.n]; + + // 3d positions + Vector3f a = Vector3f::From(curr.a); + Vector3f b = Vector3f::From(curr.b); + + // tangent + Vector3f tan = Vector3f::From(curr.b.Minus(curr.a)); + + // length + double len = curr.b.Minus(curr.a).Magnitude(); + + // make line start cap + for(int j = 0; j < 2; j++) { + vertices[curVertex + j].pos = a; + vertices[curVertex + j].tan = tan; + } + vertices[curVertex + 0].loc = Vector3f::From(-1.0f, -1.0f, float(phase)); + vertices[curVertex + 1].loc = Vector3f::From(-1.0f, +1.0f, float(phase)); + + indices[curIndex++] = curVertex + 0; + indices[curIndex++] = curVertex + 1; + indices[curIndex++] = curVertex + 2; + indices[curIndex++] = curVertex + 1; + indices[curIndex++] = curVertex + 2; + indices[curIndex++] = curVertex + 3; + + curVertex += 2; + + // make line body + vertices[curVertex + 0].pos = a; + vertices[curVertex + 1].pos = a; + vertices[curVertex + 2].pos = b; + vertices[curVertex + 3].pos = b; + + for(int j = 0; j < 4; j++) { + vertices[curVertex + j].tan = tan; + } + + vertices[curVertex + 0].loc = Vector3f::From( 0.0f, -1.0f, float(phase)); + vertices[curVertex + 1].loc = Vector3f::From( 0.0f, +1.0f, float(phase)); + vertices[curVertex + 2].loc = Vector3f::From( 0.0f, +1.0f, float(phase + len)); + vertices[curVertex + 3].loc = Vector3f::From( 0.0f, -1.0f, float(phase + len)); + + indices[curIndex++] = curVertex + 0; + indices[curIndex++] = curVertex + 1; + indices[curIndex++] = curVertex + 2; + indices[curIndex++] = curVertex + 0; + indices[curIndex++] = curVertex + 2; + indices[curIndex++] = curVertex + 3; + + curVertex += 4; + + // make line end cap + for(int j = 0; j < 2; j++) { + vertices[curVertex + j].pos = b; + vertices[curVertex + j].tan = tan; + } + + vertices[curVertex + 0].loc = Vector3f::From(+1.0, +1.0, float(phase + len)); + vertices[curVertex + 1].loc = Vector3f::From(+1.0, -1.0, float(phase + len)); + + indices[curIndex++] = curVertex - 2; + indices[curIndex++] = curVertex - 1; + indices[curIndex++] = curVertex; + indices[curIndex++] = curVertex - 1; + indices[curIndex++] = curVertex; + indices[curIndex++] = curVertex + 1; + + curVertex += 2; + + // phase stitching + if(curr.a.EqualsExactly(next.a) || + curr.a.EqualsExactly(next.b) || + curr.b.EqualsExactly(next.a) || + curr.b.EqualsExactly(next.b)) + { + phase += len; + } else { + phase = 0.0; + } + } + handle.size = curIndex; + GLenum mode = dynamic ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW; + glBufferData(GL_ARRAY_BUFFER, curVertex * sizeof(EdgeVertex), vertices, mode); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, curIndex * sizeof(uint32_t), indices, mode); + delete []vertices; + delete []indices; + + return handle; +} + +void EdgeRenderer::Remove(const EdgeRenderer::Handle &handle) { + glDeleteBuffers(1, &handle.vertexBuffer); + glDeleteBuffers(1, &handle.indexBuffer); +} + +void EdgeRenderer::Draw(const EdgeRenderer::Handle &handle) { + if(handle.size == 0) return; + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, atlas->GetTexture(pattern)); + shader.SetUniformTextureUnit("pattern", 1); + shader.SetUniformFloat("patternLen", (float)atlas->GetLength(pattern)); + + shader.Enable(); + + glBindBuffer(GL_ARRAY_BUFFER, handle.vertexBuffer); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, handle.indexBuffer); + + glEnableVertexAttribArray(ATTRIB_POS); + glEnableVertexAttribArray(ATTRIB_LOC); + glEnableVertexAttribArray(ATTRIB_TAN); + + glVertexAttribPointer(ATTRIB_POS, 3, GL_FLOAT, GL_FALSE, sizeof(EdgeVertex), (void *)offsetof(EdgeVertex, pos)); + glVertexAttribPointer(ATTRIB_LOC, 3, GL_FLOAT, GL_FALSE, sizeof(EdgeVertex), (void *)offsetof(EdgeVertex, loc)); + glVertexAttribPointer(ATTRIB_TAN, 3, GL_FLOAT, GL_FALSE, sizeof(EdgeVertex), (void *)offsetof(EdgeVertex, tan)); + glDrawElements(GL_TRIANGLES, handle.size, GL_UNSIGNED_INT, NULL); + + glDisableVertexAttribArray(ATTRIB_POS); + glDisableVertexAttribArray(ATTRIB_LOC); + glDisableVertexAttribArray(ATTRIB_TAN); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + + shader.Disable(); +} + +void EdgeRenderer::Draw(const SEdgeList &edges) { + Handle handle = Add(edges, /*dynamic=*/true); + Draw(handle); + Remove(handle); +} + +void EdgeRenderer::SetModelview(const double *matrix) { + shader.SetUniformMatrix("modelview", matrix); +} + +void EdgeRenderer::SetProjection(const double *matrix) { + shader.SetUniformMatrix("projection", matrix); +} + +void EdgeRenderer::SetStroke(const Canvas::Stroke &stroke, double pixel) { + double unitScale = stroke.unit == Canvas::Unit::PX ? pixel : 1.0; + shader.SetUniformFloat("width", float(stroke.width * unitScale / 2.0)); + shader.SetUniformColor("color", stroke.color); + shader.SetUniformFloat("patternScale", float(stroke.stippleScale * unitScale * 2.0)); + shader.SetUniformFloat("pixel", (float)pixel); + pattern = stroke.stipplePattern; +} + +//----------------------------------------------------------------------------- +// Outline rendering +//----------------------------------------------------------------------------- + +void OutlineRenderer::Init(const StippleAtlas *a) { + atlas = a; + shader.Init( + "shaders/outline.vert", "shaders/edge.frag", + { + { ATTRIB_POS, "pos" }, + { ATTRIB_LOC, "loc" }, + { ATTRIB_TAN, "tgt" }, + { ATTRIB_NOL, "nol" }, + { ATTRIB_NOR, "nor" } + } + ); +} + +void OutlineRenderer::Clear() { + shader.Clear(); +} + +OutlineRenderer::Handle OutlineRenderer::Add(const SOutlineList &outlines, bool dynamic) { + Handle handle; + glGenBuffers(1, &handle.vertexBuffer); + glGenBuffers(1, &handle.indexBuffer); + glBindBuffer(GL_ARRAY_BUFFER, handle.vertexBuffer); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, handle.indexBuffer); + + OutlineVertex *vertices = new OutlineVertex[outlines.l.n * 8]; + uint32_t *indices = new uint32_t[outlines.l.n * 6 * 3]; + double phase = 0.0; + uint32_t curVertex = 0; + uint32_t curIndex = 0; + + for(int i = 0; i < outlines.l.n; i++) { + const SOutline &curr = outlines.l[i]; + const SOutline &next = outlines.l[(i + 1) % outlines.l.n]; + + // 3d positions + Vector3f a = Vector3f::From(curr.a); + Vector3f b = Vector3f::From(curr.b); + Vector3f nl = Vector3f::From(curr.nl); + Vector3f nr = Vector3f::From(curr.nr); + + // tangent + Vector3f tan = Vector3f::From(curr.b.Minus(curr.a)); + + // length + double len = curr.b.Minus(curr.a).Magnitude(); + float tag = (float)curr.tag; + + // make line start cap + for(int j = 0; j < 2; j++) { + vertices[curVertex + j].pos = a; + vertices[curVertex + j].nol = nl; + vertices[curVertex + j].nor = nr; + vertices[curVertex + j].tan = tan; + } + + vertices[curVertex + 0].loc = Vector4f::From(-1.0f, -1.0f, float(phase), (float)tag); + vertices[curVertex + 1].loc = Vector4f::From(-1.0f, +1.0f, float(phase), (float)tag); + + indices[curIndex++] = curVertex; + indices[curIndex++] = curVertex + 1; + indices[curIndex++] = curVertex + 2; + indices[curIndex++] = curVertex + 1; + indices[curIndex++] = curVertex + 2; + indices[curIndex++] = curVertex + 3; + + curVertex += 2; + + // make line body + vertices[curVertex + 0].pos = a; + vertices[curVertex + 1].pos = a; + vertices[curVertex + 2].pos = b; + vertices[curVertex + 3].pos = b; + + for(int j = 0; j < 4; j++) { + vertices[curVertex + j].nol = nl; + vertices[curVertex + j].nor = nr; + vertices[curVertex + j].tan = tan; + } + + vertices[curVertex + 0].loc = Vector4f::From( 0.0f, -1.0f, float(phase), (float)tag); + vertices[curVertex + 1].loc = Vector4f::From( 0.0f, +1.0f, float(phase), (float)tag); + vertices[curVertex + 2].loc = Vector4f::From( 0.0f, +1.0f, + float(phase + len), (float)tag); + vertices[curVertex + 3].loc = Vector4f::From( 0.0f, -1.0f, + float(phase + len), (float)tag); + + indices[curIndex++] = curVertex + 0; + indices[curIndex++] = curVertex + 1; + indices[curIndex++] = curVertex + 2; + indices[curIndex++] = curVertex + 0; + indices[curIndex++] = curVertex + 2; + indices[curIndex++] = curVertex + 3; + + curVertex += 4; + + // make line end cap + for(int j = 0; j < 2; j++) { + vertices[curVertex + j].pos = b; + vertices[curVertex + j].nol = nl; + vertices[curVertex + j].nor = nr; + vertices[curVertex + j].tan = tan; + } + + vertices[curVertex + 0].loc = Vector4f::From(+1.0f, +1.0f, float(phase + len), (float)tag); + vertices[curVertex + 1].loc = Vector4f::From(+1.0f, -1.0f, float(phase + len), (float)tag); + + indices[curIndex++] = curVertex - 2; + indices[curIndex++] = curVertex - 1; + indices[curIndex++] = curVertex; + indices[curIndex++] = curVertex - 1; + indices[curIndex++] = curVertex; + indices[curIndex++] = curVertex + 1; + + curVertex += 2; + + // phase stitching + if(curr.a.EqualsExactly(next.a) || + curr.a.EqualsExactly(next.b) || + curr.b.EqualsExactly(next.a) || + curr.b.EqualsExactly(next.b)) + { + phase += len; + } else { + phase = 0.0; + } + } + handle.size = curIndex; + GLenum mode = dynamic ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW; + glBufferData(GL_ARRAY_BUFFER, curVertex * sizeof(OutlineVertex), vertices, mode); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, curIndex * sizeof(uint32_t), indices, mode); + + delete []vertices; + delete []indices; + return handle; +} + +void OutlineRenderer::Remove(const OutlineRenderer::Handle &handle) { + glDeleteBuffers(1, &handle.vertexBuffer); + glDeleteBuffers(1, &handle.indexBuffer); +} + +void OutlineRenderer::Draw(const OutlineRenderer::Handle &handle, Canvas::DrawOutlinesAs mode) { + if(handle.size == 0) return; + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, atlas->GetTexture(pattern)); + shader.SetUniformTextureUnit("pattern", 1); + shader.SetUniformFloat("patternLen", (float)atlas->GetLength(pattern)); + shader.SetUniformInt("mode", (GLint)mode); + + shader.Enable(); + + glBindBuffer(GL_ARRAY_BUFFER, handle.vertexBuffer); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, handle.indexBuffer); + + glEnableVertexAttribArray(ATTRIB_POS); + glEnableVertexAttribArray(ATTRIB_LOC); + glEnableVertexAttribArray(ATTRIB_TAN); + glEnableVertexAttribArray(ATTRIB_NOL); + glEnableVertexAttribArray(ATTRIB_NOR); + + glVertexAttribPointer(ATTRIB_POS, 3, GL_FLOAT, GL_FALSE, sizeof(OutlineVertex), + (void *)offsetof(OutlineVertex, pos)); + glVertexAttribPointer(ATTRIB_LOC, 4, GL_FLOAT, GL_FALSE, sizeof(OutlineVertex), + (void *)offsetof(OutlineVertex, loc)); + glVertexAttribPointer(ATTRIB_TAN, 3, GL_FLOAT, GL_FALSE, sizeof(OutlineVertex), + (void *)offsetof(OutlineVertex, tan)); + glVertexAttribPointer(ATTRIB_NOL, 3, GL_FLOAT, GL_FALSE, sizeof(OutlineVertex), + (void *)offsetof(OutlineVertex, nol)); + glVertexAttribPointer(ATTRIB_NOR, 3, GL_FLOAT, GL_FALSE, sizeof(OutlineVertex), + (void *)offsetof(OutlineVertex, nor)); + glDrawElements(GL_TRIANGLES, handle.size, GL_UNSIGNED_INT, NULL); + + glDisableVertexAttribArray(ATTRIB_POS); + glDisableVertexAttribArray(ATTRIB_LOC); + glDisableVertexAttribArray(ATTRIB_TAN); + glDisableVertexAttribArray(ATTRIB_NOL); + glDisableVertexAttribArray(ATTRIB_NOR); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + shader.Disable(); +} + +void OutlineRenderer::Draw(const SOutlineList &outlines, Canvas::DrawOutlinesAs drawAs) { + Handle handle = Add(outlines, /*dynamic=*/true); + Draw(handle, drawAs); + Remove(handle); +} + +void OutlineRenderer::SetModelview(const double *matrix) { + shader.SetUniformMatrix("modelview", matrix); +} + +void OutlineRenderer::SetProjection(const double *matrix) { + shader.SetUniformMatrix("projection", matrix); +} + +void OutlineRenderer::SetStroke(const Canvas::Stroke &stroke, double pixel) { + double unitScale = (stroke.unit == Canvas::Unit::PX) ? pixel : 1.0; + shader.SetUniformFloat("width", (float)(stroke.width * unitScale / 2.0)); + shader.SetUniformColor("color", stroke.color); + shader.SetUniformFloat("patternScale", (float)(stroke.stippleScale * unitScale * 2.0)); + shader.SetUniformFloat("pixel", (float)pixel); + pattern = stroke.stipplePattern; +} + +//----------------------------------------------------------------------------- +// Indexed mesh storage +//----------------------------------------------------------------------------- + +void SIndexedMesh::AddPoint(const Vector &p) { + uint32_t vstart = vertices.size(); + vertices.resize(vertices.size() + 4); + + vertices[vstart + 0].pos = Vector3f::From(p); + vertices[vstart + 0].tex = Vector2f::From(-1.0f, -1.0f); + vertices[vstart + 1].pos = Vector3f::From(p); + vertices[vstart + 1].tex = Vector2f::From(+1.0f, -1.0f); + vertices[vstart + 2].pos = Vector3f::From(p); + vertices[vstart + 2].tex = Vector2f::From(+1.0f, +1.0f); + vertices[vstart + 3].pos = Vector3f::From(p); + vertices[vstart + 3].tex = Vector2f::From(-1.0f, +1.0f); + + size_t istart = indices.size(); + indices.resize(indices.size() + 6); + + indices[istart + 0] = vstart + 0; + indices[istart + 1] = vstart + 1; + indices[istart + 2] = vstart + 2; + indices[istart + 3] = vstart + 0; + indices[istart + 4] = vstart + 2; + indices[istart + 5] = vstart + 3; +} + +void SIndexedMesh::AddQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d) { + uint32_t vstart = vertices.size(); + vertices.resize(vertices.size() + 4); + + vertices[vstart + 0].pos = Vector3f::From(a); + vertices[vstart + 1].pos = Vector3f::From(b); + vertices[vstart + 2].pos = Vector3f::From(c); + vertices[vstart + 3].pos = Vector3f::From(d); + + size_t istart = indices.size(); + indices.resize(indices.size() + 6); + + indices[istart + 0] = vstart + 0; + indices[istart + 1] = vstart + 1; + indices[istart + 2] = vstart + 2; + indices[istart + 3] = vstart + 0; + indices[istart + 4] = vstart + 2; + indices[istart + 5] = vstart + 3; +} + +void SIndexedMesh::AddPixmap(const Vector &o, const Vector &u, const Vector &v, + const Point2d &ta, const Point2d &tb) { + uint32_t vstart = vertices.size(); + vertices.resize(vertices.size() + 4); + + vertices[vstart + 0].pos = Vector3f::From(o); + vertices[vstart + 0].tex = Vector2f::From(ta.x, ta.y); + + vertices[vstart + 1].pos = Vector3f::From(o.Plus(v)); + vertices[vstart + 1].tex = Vector2f::From(ta.x, tb.y); + + vertices[vstart + 2].pos = Vector3f::From(o.Plus(u).Plus(v)); + vertices[vstart + 2].tex = Vector2f::From(tb.x, tb.y); + + vertices[vstart + 3].pos = Vector3f::From(o.Plus(u)); + vertices[vstart + 3].tex = Vector2f::From(tb.x, ta.y); + + size_t istart = indices.size(); + indices.resize(indices.size() + 6); + + indices[istart + 0] = vstart + 0; + indices[istart + 1] = vstart + 1; + indices[istart + 2] = vstart + 2; + indices[istart + 3] = vstart + 0; + indices[istart + 4] = vstart + 2; + indices[istart + 5] = vstart + 3; +} + +void SIndexedMesh::Clear() { + vertices.clear(); + indices.clear(); +} + +//----------------------------------------------------------------------------- +// Indexed mesh rendering +//----------------------------------------------------------------------------- + +void IndexedMeshRenderer::Init() { + colShader.Init( + "shaders/imesh.vert", "shaders/imesh.frag", + { + { ATTRIB_POS, "pos" } + } + ); + texShader.Init( + "shaders/imesh_tex.vert", "shaders/imesh_tex.frag", + { + { ATTRIB_POS, "pos" }, + { ATTRIB_TEX, "tex" } + } + ); + texaShader.Init( + "shaders/imesh_tex.vert", "shaders/imesh_texa.frag", + { + { ATTRIB_POS, "pos" }, + { ATTRIB_TEX, "tex" } + } + ); + pointShader.Init( + "shaders/imesh_point.vert", "shaders/imesh_point.frag", + { + { ATTRIB_POS, "pos" }, + { ATTRIB_TEX, "loc" } + } + ); + + texShader.SetUniformTextureUnit("texture_", 0); + texaShader.SetUniformTextureUnit("texture_", 0); + selectedShader = &colShader; +} + +void IndexedMeshRenderer::Clear() { + texShader.Clear(); + texaShader.Clear(); + colShader.Clear(); + pointShader.Clear(); +} + +IndexedMeshRenderer::Handle IndexedMeshRenderer::Add(const SIndexedMesh &m, bool dynamic) { + Handle handle; + glGenBuffers(1, &handle.vertexBuffer); + glGenBuffers(1, &handle.indexBuffer); + + GLenum mode = dynamic ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW; + glBindBuffer(GL_ARRAY_BUFFER, handle.vertexBuffer); + glBufferData(GL_ARRAY_BUFFER, m.vertices.size() * sizeof(SIndexedMesh::Vertex), + m.vertices.data(), mode); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, handle.indexBuffer); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, m.indices.size() * sizeof(uint32_t), + m.indices.data(), mode); + handle.size = m.indices.size(); + return handle; +} + +void IndexedMeshRenderer::Remove(const IndexedMeshRenderer::Handle &handle) { + glDeleteBuffers(1, &handle.vertexBuffer); + glDeleteBuffers(1, &handle.indexBuffer); +} + +void IndexedMeshRenderer::Draw(const IndexedMeshRenderer::Handle &handle) { + if(handle.size == 0) return; + + selectedShader->Enable(); + + glBindBuffer(GL_ARRAY_BUFFER, handle.vertexBuffer); + glEnableVertexAttribArray(ATTRIB_POS); + glVertexAttribPointer(ATTRIB_POS, 3, GL_FLOAT, GL_FALSE, sizeof(SIndexedMesh::Vertex), + (void *)offsetof(SIndexedMesh::Vertex, pos)); + if(NeedsTexture()) { + glEnableVertexAttribArray(ATTRIB_TEX); + glVertexAttribPointer(ATTRIB_TEX, 2, GL_FLOAT, GL_FALSE, sizeof(SIndexedMesh::Vertex), + (void *)offsetof(SIndexedMesh::Vertex, tex)); + } + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, handle.indexBuffer); + glDrawElements(GL_TRIANGLES, handle.size, GL_UNSIGNED_INT, NULL); + + glDisableVertexAttribArray(ATTRIB_POS); + if(NeedsTexture()) glDisableVertexAttribArray(ATTRIB_TEX); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + + selectedShader->Disable(); +} + +void IndexedMeshRenderer::Draw(const SIndexedMesh &mesh) { + Handle handle = Add(mesh, /*dynamic=*/true) ; + Draw(handle); + Remove(handle); +} + +bool IndexedMeshRenderer::NeedsTexture() const { + return selectedShader == &texShader || + selectedShader == &texaShader || + selectedShader == &pointShader; +} + +void IndexedMeshRenderer::SetModelview(const double *matrix) { + colShader.SetUniformMatrix("modelview", matrix); + texShader.SetUniformMatrix("modelview", matrix); + texaShader.SetUniformMatrix("modelview", matrix); + pointShader.SetUniformMatrix("modelview", matrix); +} + +void IndexedMeshRenderer::SetProjection(const double *matrix) { + colShader.SetUniformMatrix("projection", matrix); + texShader.SetUniformMatrix("projection", matrix); + texaShader.SetUniformMatrix("projection", matrix); + pointShader.SetUniformMatrix("projection", matrix); +} + +void IndexedMeshRenderer::UseFilled(const Canvas::Fill &fill) { + if(fill.texture) { + selectedShader = (fill.texture->format == Pixmap::Format::A) ? &texaShader : &texShader; + } else { + selectedShader = &colShader; + } + selectedShader->SetUniformColor("color", fill.color); +} + +void IndexedMeshRenderer::UsePoint(const Canvas::Stroke &stroke, double pixel) { + pointShader.SetUniformColor("color", stroke.color); + pointShader.SetUniformFloat("width", (float)(stroke.width * pixel / 2.0)); + pointShader.SetUniformFloat("pixel", (float)pixel); + selectedShader = &pointShader; +} + +} diff --git a/src/render/gl3shader.h b/src/render/gl3shader.h new file mode 100644 index 0000000..19f7312 --- /dev/null +++ b/src/render/gl3shader.h @@ -0,0 +1,271 @@ +//----------------------------------------------------------------------------- +// OpenGL ES 2.0 and OpenGL 3.0 shader interface. +// +// Copyright 2016 Aleksey Egorov +//----------------------------------------------------------------------------- +#ifndef SOLVESPACE_GL3SHADER_H +#define SOLVESPACE_GL3SHADER_H + +#if defined(WIN32) || defined(__EMSCRIPTEN__) +# define GL_APICALL /*static linkage*/ +# define GL_GLEXT_PROTOTYPES +# include +# include +# define HAVE_GLES +#elif defined(__APPLE__) +# include +#else +# define GL_GLEXT_PROTOTYPES +# include +# include +#endif + +#include +#include + +#include "sketch.h" + +#if !defined(HAVE_GLES) +// glDepthRange is in GL1+ but not GLES2, glDepthRangef is in GL4.1+ and GLES2. +// Consistency! +# define glClearDepthf glClearDepth +# define glDepthRangef glDepthRange +#endif + +namespace SolveSpace { + +//----------------------------------------------------------------------------- +// Floating-point data structures; the layout of these must match shaders +//----------------------------------------------------------------------------- + +class Vector2f { +public: + float x, y; + + static Vector2f From(float x, float y); + static Vector2f From(double x, double y); + static Vector2f FromInt(uint32_t x, uint32_t y); +}; + +class Vector3f { +public: + float x, y, z; + + static Vector3f From(float x, float y, float z); + static Vector3f From(const Vector &v); + static Vector3f From(const RgbaColor &c); +}; + +class Vector4f { +public: + float x, y, z, w; + + static Vector4f From(float x, float y, float z, float w); + static Vector4f From(const Vector &v, float w); + static Vector4f FromInt(uint32_t x, uint32_t y, uint32_t z, uint32_t w); + static Vector4f From(const RgbaColor &c); +}; + +//----------------------------------------------------------------------------- +// Wrappers for our shaders +//----------------------------------------------------------------------------- + +class Shader { +public: + GLuint program = 0; + + void Init(const std::string &vertexRes, + const std::string &fragmentRes, + const std::vector> &locations = {}); + void Clear(); + + void SetUniformMatrix(const char *name, const double *md); + void SetUniformVector(const char *name, const Vector &v); + void SetUniformVector(const char *name, const Vector4f &v); + void SetUniformColor(const char *name, RgbaColor c); + void SetUniformFloat(const char *name, float v); + void SetUniformInt(const char *name, GLint v); + void SetUniformTextureUnit(const char *name, GLint index); + void Enable() const; + void Disable() const; +}; + +class MeshRenderer { +public: + const GLint ATTRIB_POS = 0; + const GLint ATTRIB_NOR = 1; + const GLint ATTRIB_COL = 2; + + struct MeshVertex { + Vector3f pos; + Vector3f nor; + Vector4f col; + }; + + struct Handle { + GLuint vertexBuffer; + GLsizei size; + }; + + Shader lightShader; + Shader fillShader; + Shader *selectedShader = NULL; + + void Init(); + void Clear(); + + Handle Add(const SMesh &m, bool dynamic = false); + void Remove(const Handle &handle); + void Draw(const Handle &handle, bool useColors = true, RgbaColor overrideColor = {}); + void Draw(const SMesh &mesh, bool useColors = true, RgbaColor overrideColor = {}); + + void SetModelview(double *matrix); + void SetProjection(double *matrix); + + void UseShaded(const Lighting &lighting); + void UseFilled(const Canvas::Fill &fill); +}; + +class StippleAtlas { +public: + std::vector patterns; + + void Init(); + void Clear(); + + GLint GetTexture(StipplePattern pattern) const; + double GetLength(StipplePattern pattern) const; +}; + +class EdgeRenderer { +public: + const GLint ATTRIB_POS = 0; + const GLint ATTRIB_LOC = 1; + const GLint ATTRIB_TAN = 2; + + struct EdgeVertex { + Vector3f pos; + Vector3f loc; + Vector3f tan; + }; + + struct Handle { + GLuint vertexBuffer; + GLuint indexBuffer; + GLsizei size; + }; + + Shader shader; + + const StippleAtlas *atlas = NULL; + StipplePattern pattern = StipplePattern::CONTINUOUS; + + void Init(const StippleAtlas *atlas); + void Clear(); + + Handle Add(const SEdgeList &edges, bool dynamic = false); + void Remove(const Handle &handle); + void Draw(const Handle &handle); + void Draw(const SEdgeList &edges); + + void SetModelview(const double *matrix); + void SetProjection(const double *matrix); + void SetStroke(const Canvas::Stroke &stroke, double pixel); +}; + +class OutlineRenderer { +public: + const GLint ATTRIB_POS = 0; + const GLint ATTRIB_LOC = 1; + const GLint ATTRIB_TAN = 2; + const GLint ATTRIB_NOL = 3; + const GLint ATTRIB_NOR = 4; + + struct OutlineVertex { + Vector3f pos; + Vector4f loc; + Vector3f tan; + Vector3f nol; + Vector3f nor; + }; + + struct Handle { + GLuint vertexBuffer; + GLuint indexBuffer; + GLsizei size; + }; + + Shader shader; + + const StippleAtlas *atlas = NULL; + StipplePattern pattern = StipplePattern::CONTINUOUS; + + void Init(const StippleAtlas *atlas); + void Clear(); + + Handle Add(const SOutlineList &outlines, bool dynamic = false); + void Remove(const Handle &handle); + void Draw(const Handle &handle, Canvas::DrawOutlinesAs mode); + void Draw(const SOutlineList &outlines, Canvas::DrawOutlinesAs mode); + + void SetModelview(const double *matrix); + void SetProjection(const double *matrix); + void SetStroke(const Canvas::Stroke &stroke, double pixel); +}; + +class SIndexedMesh { +public: + struct Vertex { + Vector3f pos; + Vector2f tex; + }; + + std::vector vertices; + std::vector indices; + + void AddPoint(const Vector &p); + void AddQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d); + void AddPixmap(const Vector &o, const Vector &u, const Vector &v, + const Point2d &ta, const Point2d &tb); + + void Clear(); +}; + +class IndexedMeshRenderer { +public: + const GLint ATTRIB_POS = 0; + const GLint ATTRIB_TEX = 1; + + struct Handle { + GLuint vertexBuffer; + GLuint indexBuffer; + GLsizei size; + }; + + Shader texShader; + Shader texaShader; + Shader colShader; + Shader pointShader; + + Shader *selectedShader = NULL; + + void Init(); + void Clear(); + + Handle Add(const SIndexedMesh &m, bool dynamic = false); + void Remove(const Handle &handle); + void Draw(const Handle &handle); + void Draw(const SIndexedMesh &mesh); + + void SetModelview(const double *matrix); + void SetProjection(const double *matrix); + + bool NeedsTexture() const; + + void UseFilled(const Canvas::Fill &fill); + void UsePoint(const Canvas::Stroke &stroke, double pixel); +}; + +} + +#endif diff --git a/src/render/render.cpp b/src/render/render.cpp new file mode 100644 index 0000000..89434c2 --- /dev/null +++ b/src/render/render.cpp @@ -0,0 +1,459 @@ +//----------------------------------------------------------------------------- +// Backend-agnostic rendering interface, and various backends we use. +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +//----------------------------------------------------------------------------- +// Camera transformations. +//----------------------------------------------------------------------------- + +Point2d Camera::ProjectPoint(Vector p) const { + Vector p3 = ProjectPoint3(p); + Point2d p2 = { p3.x, p3.y }; + return p2; +} + +Vector Camera::ProjectPoint3(Vector p) const { + double w; + Vector r = ProjectPoint4(p, &w); + return r.ScaledBy(scale/w); +} + +Vector Camera::ProjectPoint4(Vector p, double *w) const { + p = p.Plus(offset); + + Vector r; + r.x = p.Dot(projRight); + r.y = p.Dot(projUp); + r.z = p.Dot(projUp.Cross(projRight)); + + *w = 1 + r.z*tangent*scale; + return r; +} + +Vector Camera::UnProjectPoint(Point2d p) const { + Vector orig = offset.ScaledBy(-1); + + // Note that we're ignoring the effects of perspective. Since our returned + // point has the same component normal to the screen as the offset, it + // will have z = 0 after the rotation is applied, thus w = 1. So this is + // correct. + orig = orig.Plus(projRight.ScaledBy(p.x / scale)).Plus( + projUp. ScaledBy(p.y / scale)); + return orig; +} + +Vector Camera::UnProjectPoint3(Vector p) const { + p.z = p.z / (scale - p.z * tangent * scale); + double w = 1 + p.z * tangent * scale; + p.x *= w / scale; + p.y *= w / scale; + + Vector orig = offset.ScaledBy(-1); + orig = orig.Plus(projRight.ScaledBy(p.x)).Plus( + projUp. ScaledBy(p.y).Plus( + projUp.Cross(projRight). ScaledBy(p.z))); + return orig; +} + +Vector Camera::VectorFromProjs(Vector rightUpForward) const { + Vector n = projRight.Cross(projUp); + + Vector r = (projRight.ScaledBy(rightUpForward.x)); + r = r.Plus(projUp.ScaledBy(rightUpForward.y)); + r = r.Plus(n.ScaledBy(rightUpForward.z)); + return r; +} + +Vector Camera::AlignToPixelGrid(Vector v) const { + if(!gridFit) return v; + + v = ProjectPoint3(v); + v.x = floor(v.x) + 0.5; + v.y = floor(v.y) + 0.5; + return UnProjectPoint3(v); +} + +SBezier Camera::ProjectBezier(SBezier b) const { + Quaternion q = Quaternion::From(projRight, projUp); + q = q.Inverse(); + // we want Q*(p - o) = Q*p - Q*o + b = b.TransformedBy(q.Rotate(offset).ScaledBy(scale), q, scale); + for(int i = 0; i <= b.deg; i++) { + Vector4 ct = Vector4::From(b.weight[i], b.ctrl[i]); + // so the desired curve, before perspective, is + // (x/w, y/w, z/w) + // and after perspective is + // ((x/w)/(1 - (z/w)*tangent, ... + // = (x/(w - z*tangent), ... + // so we want to let w' = w - z*tangent + ct.w = ct.w - ct.z*tangent; + + b.ctrl[i] = ct.PerspectiveProject(); + b.weight[i] = ct.w; + } + return b; +} + +void Camera::LoadIdentity() { + offset = { 0.0, 0.0, 0.0 }; + projRight = { 1.0, 0.0, 0.0 }; + projUp = { 0.0, 1.0, 0.0 }; + scale = 1.0; + tangent = 0.0; +} + +void Camera::NormalizeProjectionVectors() { + if(projRight.Magnitude() < LENGTH_EPS) { + projRight = Vector::From(1, 0, 0); + } + + Vector norm = projRight.Cross(projUp); + // If projRight and projUp somehow ended up parallel, then pick an + // arbitrary projUp normal to projRight. + if(norm.Magnitude() < LENGTH_EPS) { + norm = projRight.Normal(0); + } + projUp = norm.Cross(projRight); + + projUp = projUp.WithMagnitude(1); + projRight = projRight.WithMagnitude(1); +} + +//----------------------------------------------------------------------------- +// Stroke and fill caching. +//----------------------------------------------------------------------------- + +bool Canvas::Stroke::Equals(const Stroke &other) const { + return (layer == other.layer && + zIndex == other.zIndex && + color.Equals(other.color) && + width == other.width && + unit == other.unit && + stipplePattern == other.stipplePattern && + stippleScale == other.stippleScale); +} + +double Canvas::Stroke::WidthMm(const Camera &camera) const { + switch(unit) { + case Canvas::Unit::MM: + return width; + case Canvas::Unit::PX: + return width / camera.scale; + default: + ssassert(false, "Unexpected unit"); + } +} + +double Canvas::Stroke::WidthPx(const Camera &camera) const { + switch(unit) { + case Canvas::Unit::MM: + return width * camera.scale; + case Canvas::Unit::PX: + return width; + default: + ssassert(false, "Unexpected unit"); + } +} + +double Canvas::Stroke::StippleScaleMm(const Camera &camera) const { + switch(unit) { + case Canvas::Unit::MM: + return stippleScale; + case Canvas::Unit::PX: + return stippleScale / camera.scale; + default: + ssassert(false, "Unexpected unit"); + } +} + +double Canvas::Stroke::StippleScalePx(const Camera &camera) const { + switch(unit) { + case Canvas::Unit::MM: + return stippleScale * camera.scale; + case Canvas::Unit::PX: + return stippleScale; + default: + ssassert(false, "Unexpected unit"); + } +} + +bool Canvas::Fill::Equals(const Fill &other) const { + return (layer == other.layer && + zIndex == other.zIndex && + color.Equals(other.color) && + pattern == other.pattern && + texture == other.texture); +} + +void Canvas::Clear() { + strokes.Clear(); + fills.Clear(); +} + +Canvas::hStroke Canvas::GetStroke(const Stroke &stroke) { + for(const Stroke &s : strokes) { + if(s.Equals(stroke)) return s.h; + } + Stroke strokeCopy = stroke; + return strokes.AddAndAssignId(&strokeCopy); +} + +Canvas::hFill Canvas::GetFill(const Fill &fill) { + for(const Fill &f : fills) { + if(f.Equals(fill)) return f.h; + } + Fill fillCopy = fill; + return fills.AddAndAssignId(&fillCopy); +} + +BitmapFont *Canvas::GetBitmapFont() { + if(bitmapFont.IsEmpty()) { + bitmapFont = BitmapFont::Create(); + } + return &bitmapFont; +} + +std::shared_ptr Canvas::CreateBatch() { + return std::shared_ptr(); +} + +//----------------------------------------------------------------------------- +// An interface for view-independent visualization +//----------------------------------------------------------------------------- + +const Camera &BatchCanvas::GetCamera() const { + ssassert(false, "Geometry drawn on BatchCanvas must be independent from camera"); +} + +//----------------------------------------------------------------------------- +// A wrapper around Canvas that simplifies drawing UI in screen coordinates +//----------------------------------------------------------------------------- + +void UiCanvas::DrawLine(int x1, int y1, int x2, int y2, RgbaColor color, int width, int zIndex) { + Vector va = { (double)x1 + 0.5, (double)Flip(y1) + 0.5, 0.0 }, + vb = { (double)x2 + 0.5, (double)Flip(y2) + 0.5, 0.0 }; + + Canvas::Stroke stroke = {}; + stroke.layer = Canvas::Layer::NORMAL; + stroke.zIndex = zIndex; + stroke.width = (double)width; + stroke.color = color; + stroke.unit = Canvas::Unit::PX; + Canvas::hStroke hcs = canvas->GetStroke(stroke); + + canvas->DrawLine(va, vb, hcs); +} + +void UiCanvas::DrawRect(int l, int r, int t, int b, RgbaColor fillColor, RgbaColor outlineColor, + int zIndex) { + Vector va = { (double)l + 0.5, (double)Flip(b) + 0.5, 0.0 }, + vb = { (double)l + 0.5, (double)Flip(t) + 0.5, 0.0 }, + vc = { (double)r + 0.5, (double)Flip(t) + 0.5, 0.0 }, + vd = { (double)r + 0.5, (double)Flip(b) + 0.5, 0.0 }; + + if(!fillColor.IsEmpty()) { + Canvas::Fill fill = {}; + fill.layer = Canvas::Layer::NORMAL; + fill.zIndex = zIndex; + fill.color = fillColor; + Canvas::hFill hcf = canvas->GetFill(fill); + + canvas->DrawQuad(va, vb, vc, vd, hcf); + } + + if(!outlineColor.IsEmpty()) { + Canvas::Stroke stroke = {}; + stroke.layer = Canvas::Layer::NORMAL; + stroke.zIndex = zIndex; + stroke.width = 1.0; + stroke.color = outlineColor; + stroke.unit = Canvas::Unit::PX; + Canvas::hStroke hcs = canvas->GetStroke(stroke); + + canvas->DrawLine(va, vb, hcs); + canvas->DrawLine(vb, vc, hcs); + canvas->DrawLine(vc, vd, hcs); + canvas->DrawLine(vd, va, hcs); + } +} + +void UiCanvas::DrawPixmap(std::shared_ptr pm, int x, int y, int zIndex) { + Canvas::Fill fill = {}; + fill.layer = Canvas::Layer::NORMAL; + fill.zIndex = zIndex; + fill.color = { 255, 255, 255, 255 }; + Canvas::hFill hcf = canvas->GetFill(fill); + + canvas->DrawPixmap(pm, + { (double)x, (double)(flip ? Flip(y) - pm->height : y), 0.0 }, + { (double)pm->width, 0.0, 0.0 }, + { 0.0, (double)pm->height, 0.0 }, + { 0.0, 1.0 }, + { 1.0, 0.0 }, + hcf); +} + +void UiCanvas::DrawBitmapChar(char32_t codepoint, int x, int y, RgbaColor color, int zIndex) { + BitmapFont *font = canvas->GetBitmapFont(); + + Canvas::Fill fill = {}; + fill.layer = Canvas::Layer::NORMAL; + fill.zIndex = zIndex; + fill.color = color; + Canvas::hFill hcf = canvas->GetFill(fill); + + if(codepoint >= 0xe000 && codepoint <= 0xefff) { + // Special character, like a checkbox or a radio button + x -= 3; + } + + double s0, t0, s1, t1; + size_t w, h; + font->LocateGlyph(codepoint, &s0, &t0, &s1, &t1, &w, &h); + if(font->textureUpdated) { + // LocateGlyph modified the texture, reload it. + canvas->InvalidatePixmap(font->texture); + font->textureUpdated = false; + } + + canvas->DrawPixmap(font->texture, + { (double)x, (double)Flip(y), 0.0 }, + { (double)w, 0.0, 0.0 }, + { 0.0, (double) h, 0.0 }, + { s0, t1 }, + { s1, t0 }, + hcf); +} + +void UiCanvas::DrawBitmapText(const std::string &str, int x, int y, RgbaColor color, int zIndex) { + BitmapFont *font = canvas->GetBitmapFont(); + + for(char32_t codepoint : ReadUTF8(str)) { + DrawBitmapChar(codepoint, x, y, color, zIndex); + x += (int)font->GetWidth(codepoint) * 8; + } +} + +//----------------------------------------------------------------------------- +// A canvas that performs picking against drawn geometry. +//----------------------------------------------------------------------------- + +void ObjectPicker::DoCompare(double depth, double distance, int zIndex, int comparePosition) { + if(distance > selRadius) return; + if((zIndex == maxZIndex && distance < minDistance) || (zIndex > maxZIndex)) { + minDepth = depth; + minDistance = distance; + maxZIndex = zIndex; + position = comparePosition; + } +} + +void ObjectPicker::DoQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + int zIndex, int comparePosition) { + Point2d corners[4] = { + camera.ProjectPoint(a), + camera.ProjectPoint(b), + camera.ProjectPoint(c), + camera.ProjectPoint(d) + }; + double minNegative = VERY_NEGATIVE, + maxPositive = VERY_POSITIVE; + for(int i = 0; i < 4; i++) { + Point2d ap = corners[i], + bp = corners[(i + 1) % 4]; + double distance = point.DistanceToLineSigned(ap, bp.Minus(ap), /*asSegment=*/true); + if(distance < 0) minNegative = std::max(minNegative, distance); + if(distance > 0) maxPositive = std::min(maxPositive, distance); + } + + bool insideQuad = (minNegative == VERY_NEGATIVE || maxPositive == VERY_POSITIVE); + if(insideQuad) { + DoCompare(0, 0.0, zIndex, comparePosition); + } else { + double distance = std::min(fabs(minNegative), fabs(maxPositive)); + DoCompare(0, distance, zIndex, comparePosition); + } +} + +void ObjectPicker::DrawLine(const Vector &a, const Vector &b, hStroke hcs) { + Stroke *stroke = strokes.FindById(hcs); + Point2d ap = camera.ProjectPoint(a); + Point2d bp = camera.ProjectPoint(b); + double distance = point.DistanceToLine(ap, bp.Minus(ap), /*asSegment=*/true); + double depth = 0.5 * (camera.ProjectPoint3(a).z + camera.ProjectPoint3(b).z) ; + DoCompare(depth, distance - stroke->width / 2.0, stroke->zIndex); +} + +void ObjectPicker::DrawEdges(const SEdgeList &el, hStroke hcs) { + Stroke *stroke = strokes.FindById(hcs); + for(const SEdge &e : el.l) { + Point2d ap = camera.ProjectPoint(e.a); + Point2d bp = camera.ProjectPoint(e.b); + double distance = point.DistanceToLine(ap, bp.Minus(ap), /*asSegment=*/true); + double depth = 0.5 * (camera.ProjectPoint3(e.a).z + camera.ProjectPoint3(e.b).z) ; + DoCompare(depth, distance - stroke->width / 2.0, stroke->zIndex, e.auxB); + } +} + +void ObjectPicker::DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs drawAs) { + ssassert(false, "Not implemented"); +} + +void ObjectPicker::DrawVectorText(const std::string &text, double height, + const Vector &o, const Vector &u, const Vector &v, + hStroke hcs) { + Stroke *stroke = strokes.FindById(hcs); + double w = VectorFont::Builtin()-> GetWidth(height, text), + h = VectorFont::Builtin()->GetHeight(height); + DoQuad(o, + o.Plus(v.ScaledBy(h)), + o.Plus(u.ScaledBy(w)).Plus(v.ScaledBy(h)), + o.Plus(u.ScaledBy(w)), + stroke->zIndex); +} + +void ObjectPicker::DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + hFill hcf) { + Fill *fill = fills.FindById(hcf); + DoQuad(a, b, c, d, fill->zIndex); +} + +void ObjectPicker::DrawPoint(const Vector &o, Canvas::hStroke hcs) { + Stroke *stroke = strokes.FindById(hcs); + double distance = point.DistanceTo(camera.ProjectPoint(o)) - stroke->width / 2; + double depth = camera.ProjectPoint3(o).z; + DoCompare(depth, distance, stroke->zIndex); +} + +void ObjectPicker::DrawPolygon(const SPolygon &p, hFill hcf) { + ssassert(false, "Not implemented"); +} + +void ObjectPicker::DrawMesh(const SMesh &m, hFill hcfFront, hFill hcfBack) { + ssassert(false, "Not implemented"); +} + +void ObjectPicker::DrawFaces(const SMesh &m, const std::vector &faces, hFill hcf) { + ssassert(false, "Not implemented"); +} + +void ObjectPicker::DrawPixmap(std::shared_ptr pm, + const Vector &o, const Vector &u, const Vector &v, + const Point2d &ta, const Point2d &tb, Canvas::hFill hcf) { + DrawQuad(o, o.Plus(u), o.Plus(u).Plus(v), o.Plus(v), hcf); +} + +bool ObjectPicker::Pick(const std::function &drawFn) { + minDepth = VERY_POSITIVE; + minDistance = VERY_POSITIVE; + maxZIndex = INT_MIN; + + drawFn(); + return minDistance < selRadius; +} +} diff --git a/src/render/render.h b/src/render/render.h new file mode 100644 index 0000000..6a4ef62 --- /dev/null +++ b/src/render/render.h @@ -0,0 +1,408 @@ +//----------------------------------------------------------------------------- +// Backend-agnostic rendering interface, and various backends we use. +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- + +#ifndef SOLVESPACE_RENDER_H +#define SOLVESPACE_RENDER_H + +#include +#include +#include +#include +#include +#include + +#include "dsc.h" +#include "resource.h" +#include "srf/surface.h" + +typedef struct _cairo cairo_t; +typedef struct _cairo_surface cairo_surface_t; + +namespace SolveSpace { + +//----------------------------------------------------------------------------- +// Interfaces common for all renderers +//----------------------------------------------------------------------------- + +class Pixmap; + +enum class StipplePattern : uint32_t; + +// A mapping from 3d sketch coordinates to 2d screen coordinates, using +// an axonometric projection. +class Camera { +public: + double width; + double height; + double pixelRatio; + bool gridFit; + Vector offset; + Vector projRight; + Vector projUp; + double scale; + double tangent; + + bool IsPerspective() const { return tangent != 0.0; } + + Point2d ProjectPoint(Vector p) const; + Vector ProjectPoint3(Vector p) const; + Vector ProjectPoint4(Vector p, double *w) const; + Vector UnProjectPoint(Point2d p) const; + Vector UnProjectPoint3(Vector p) const; + Vector VectorFromProjs(Vector rightUpForward) const; + Vector AlignToPixelGrid(Vector v) const; + + SBezier ProjectBezier(SBezier b) const; + + void LoadIdentity(); + void NormalizeProjectionVectors(); +}; + +// A description of scene lighting. +class Lighting { +public: + RgbaColor backgroundColor; + double ambientIntensity; + double lightIntensity[2]; + Vector lightDirection[2]; +}; + +class BatchCanvas; + +// An interface for populating a drawing area with geometry. +class Canvas { +public: + // Stroke and fill styles are addressed with handles to be able to quickly + // group geometry into indexed draw calls. + class hStroke { + public: + uint32_t v; + }; + + class hFill { + public: + uint32_t v; + }; + + // The layer of a geometry describes how it occludes other geometry. + // Within a layer, geometry with higher z-index occludes geometry with lower z-index, + // or geometry drawn earlier if z-indexes match. + enum class Layer { + NORMAL, // Occluded by geometry with lower Z coordinate + OCCLUDED, // Only drawn over geometry with lower Z coordinate + DEPTH_ONLY, // Like NORMAL, but only affects future occlusion, not color + BACK, // Always drawn below all other geometry + FRONT, // Always drawn above all other geometry + LAST = FRONT + }; + + // The outlines are the collection of all edges that may be drawn. + // Outlines can be classified as emphasized or not; emphasized outlines indicate an abrupt + // change in the surface curvature. These are indicated by the SOutline tag. + // Outlines can also be classified as contour or not; contour outlines indicate the boundary + // of the filled mesh. Whether an outline is a part of contour or not depends on point of view. + enum class DrawOutlinesAs { + EMPHASIZED_AND_CONTOUR = 0, // Both emphasized and contour outlines + EMPHASIZED_WITHOUT_CONTOUR = 1, // Emphasized outlines except those also belonging to contour + CONTOUR_ONLY = 2 // Contour outlines only + }; + + // Stroke widths, etc, can be scale-invariant (in pixels) or scale-dependent (in millimeters). + enum class Unit { + MM, + PX + }; + + class Stroke { + public: + hStroke h; + + Layer layer; + int zIndex; + RgbaColor color; + double width; + Unit unit; + StipplePattern stipplePattern; + double stippleScale; + + void Clear() { *this = {}; } + bool Equals(const Stroke &other) const; + + double WidthMm(const Camera &camera) const; + double WidthPx(const Camera &camera) const; + double StippleScaleMm(const Camera &camera) const; + double StippleScalePx(const Camera &camera) const; + }; + + enum class FillPattern { + SOLID, CHECKERED_A, CHECKERED_B + }; + + class Fill { + public: + hFill h; + + Layer layer; + int zIndex; + RgbaColor color; + FillPattern pattern; + std::shared_ptr texture; + + void Clear() { *this = {}; } + bool Equals(const Fill &other) const; + }; + + IdList strokes = {}; + IdList fills = {}; + BitmapFont bitmapFont = {}; + + virtual void Clear(); + virtual ~Canvas() = default; + + hStroke GetStroke(const Stroke &stroke); + hFill GetFill(const Fill &fill); + BitmapFont *GetBitmapFont(); + + virtual const Camera &GetCamera() const = 0; + + virtual void DrawLine(const Vector &a, const Vector &b, hStroke hcs) = 0; + virtual void DrawEdges(const SEdgeList &el, hStroke hcs) = 0; + virtual bool DrawBeziers(const SBezierList &bl, hStroke hcs) = 0; + virtual void DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs drawAs) = 0; + virtual void DrawVectorText(const std::string &text, double height, + const Vector &o, const Vector &u, const Vector &v, + hStroke hcs) = 0; + + virtual void DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + hFill hcf) = 0; + virtual void DrawPoint(const Vector &o, hStroke hcs) = 0; + virtual void DrawPolygon(const SPolygon &p, hFill hcf) = 0; + virtual void DrawMesh(const SMesh &m, hFill hcfFront, hFill hcfBack = {}) = 0; + virtual void DrawFaces(const SMesh &m, const std::vector &faces, hFill hcf) = 0; + + virtual void DrawPixmap(std::shared_ptr pm, + const Vector &o, const Vector &u, const Vector &v, + const Point2d &ta, const Point2d &tb, hFill hcf) = 0; + virtual void InvalidatePixmap(std::shared_ptr pm) = 0; + + virtual std::shared_ptr CreateBatch(); +}; + +template<> +struct IsHandleOracle : std::true_type {}; + +template<> +struct IsHandleOracle : std::true_type {}; + + +// An interface for view-dependent visualization. +class ViewportCanvas : public Canvas { +public: + virtual void SetCamera(const Camera &camera) = 0; + virtual void SetLighting(const Lighting &lighting) = 0; + + virtual void StartFrame() = 0; + virtual void FlushFrame() = 0; + virtual void FinishFrame() = 0; + virtual std::shared_ptr ReadFrame() = 0; + + virtual void GetIdent(const char **vendor, const char **renderer, const char **version) = 0; +}; + +// An interface for view-independent visualization. +class BatchCanvas : public Canvas { +public: + const Camera &GetCamera() const override; + + virtual void Finalize() = 0; + virtual void Draw() = 0; +}; + +// A wrapper around Canvas that simplifies drawing UI in screen coordinates. +class UiCanvas { +public: + std::shared_ptr canvas; + bool flip = false; + + void DrawLine(int x1, int y1, int x2, int y2, RgbaColor color, int width = 1, + int zIndex = 0); + void DrawRect(int l, int r, int t, int b, RgbaColor fillColor, RgbaColor outlineColor, + int zIndex = 0); + void DrawPixmap(std::shared_ptr pm, int x, int y, + int zIndex = 0); + void DrawBitmapChar(char32_t codepoint, int x, int y, RgbaColor color, + int zIndex = 0); + void DrawBitmapText(const std::string &str, int x, int y, RgbaColor color, + int zIndex = 0); + + int Flip(int y) const { return flip ? (int)canvas->GetCamera().height - y : y; } +}; + +// A canvas that performs picking against drawn geometry. +class ObjectPicker : public Canvas { +public: + Camera camera = {}; + // Configuration. + Point2d point = {}; + double selRadius = 0.0; + // Picking state. + double minDistance = 0.0; + double minDepth = 1e10; + int maxZIndex = 0; + uint32_t position = 0; + + const Camera &GetCamera() const override { return camera; } + + void DrawLine(const Vector &a, const Vector &b, hStroke hcs) override; + void DrawEdges(const SEdgeList &el, hStroke hcs) override; + bool DrawBeziers(const SBezierList &bl, hStroke hcs) override { return false; } + void DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs drawAs) override; + void DrawVectorText(const std::string &text, double height, + const Vector &o, const Vector &u, const Vector &v, + hStroke hcs) override; + + void DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + hFill hcf) override; + void DrawPoint(const Vector &o, hStroke hcs) override; + void DrawPolygon(const SPolygon &p, hFill hcf) override; + void DrawMesh(const SMesh &m, hFill hcfFront, hFill hcfBack) override; + void DrawFaces(const SMesh &m, const std::vector &faces, hFill hcf) override; + + void DrawPixmap(std::shared_ptr pm, + const Vector &o, const Vector &u, const Vector &v, + const Point2d &ta, const Point2d &tb, hFill hcf) override; + void InvalidatePixmap(std::shared_ptr pm) override {} + + void DoCompare(double depth, double distance, int zIndex, int comparePosition = 0); + void DoQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + int zIndex, int comparePosition = 0); + + bool Pick(const std::function &drawFn); +}; + +template +using handle_map = std::map; + +// A canvas that renders onto a 2d surface, performing z-index sorting, occlusion testing, etc, +// on the CPU. +class SurfaceRenderer : public ViewportCanvas { +public: + Camera camera = {}; + Lighting lighting = {}; + // Chord tolerance, for converting beziers to pwl. + double chordTolerance = 0.0; + // Render lists. + handle_map edges; + handle_map beziers; + SMesh mesh = {}; + // State. + BBox bbox = {}; + + void Clear() override; + + // Canvas interface. + const Camera &GetCamera() const override { return camera; } + + // ViewportCanvas interface. + void SetCamera(const Camera &cam) override { this->camera = cam; } + void SetLighting(const Lighting &light) override { this->lighting = light; } + + void DrawLine(const Vector &a, const Vector &b, hStroke hcs) override; + void DrawEdges(const SEdgeList &el, hStroke hcs) override; + bool DrawBeziers(const SBezierList &bl, hStroke hcs) override; + void DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs drawAs) override; + void DrawVectorText(const std::string &text, double height, + const Vector &o, const Vector &u, const Vector &v, + hStroke hcs) override; + + void DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + hFill hcf) override; + void DrawPoint(const Vector &o, hStroke hcs) override; + void DrawPolygon(const SPolygon &p, hFill hcf) override; + void DrawMesh(const SMesh &m, hFill hcfFront, hFill hcfBack) override; + void DrawFaces(const SMesh &m, const std::vector &faces, hFill hcf) override; + + void DrawPixmap(std::shared_ptr pm, + const Vector &o, const Vector &u, const Vector &v, + const Point2d &ta, const Point2d &tb, hFill hcf) override; + void InvalidatePixmap(std::shared_ptr pm) override; + + // Geometry manipulation. + void CalculateBBox(); + void ConvertBeziersToEdges(); + void CullOccludedStrokes(); + + // Renderer operations. + void OutputInPaintOrder(); + + virtual bool CanOutputCurves() const = 0; + virtual bool CanOutputTriangles() const = 0; + + virtual void OutputStart() = 0; + virtual void OutputBezier(const SBezier &b, hStroke hcs) = 0; + virtual void OutputTriangle(const STriangle &tr) = 0; + virtual void OutputEnd() = 0; + + void OutputBezierAsNonrationalCubic(const SBezier &b, hStroke hcs); +}; + +//----------------------------------------------------------------------------- +// 2d renderers +//----------------------------------------------------------------------------- + +class CairoRenderer : public SurfaceRenderer { +public: + cairo_t *context = NULL; + // Renderer configuration. + bool antialias = false; + // Renderer state. + struct { + hStroke hcs; + } current = {}; + + void Clear() override; + + void StartFrame() override {} + void FlushFrame() override; + void FinishFrame() override {} + std::shared_ptr ReadFrame() override; + + void GetIdent(const char **vendor, const char **renderer, const char **version) override; + + void SelectStroke(hStroke hcs); + void MoveTo(Vector p); + void FinishPath(); + + bool CanOutputCurves() const override { return true; } + bool CanOutputTriangles() const override { return true; } + + void OutputStart() override; + void OutputBezier(const SBezier &b, hStroke hcs) override; + void OutputTriangle(const STriangle &tr) override; + void OutputEnd() override; +}; + +class CairoPixmapRenderer final : public CairoRenderer { +public: + std::shared_ptr pixmap; + + cairo_surface_t *surface = NULL; + + void Init(); + void Clear() override; + + std::shared_ptr ReadFrame() override; +}; + +//----------------------------------------------------------------------------- +// Factories +//----------------------------------------------------------------------------- + +std::shared_ptr CreateRenderer(); + +} // namespace SolveSpace + +#endif diff --git a/src/render/render2d.cpp b/src/render/render2d.cpp new file mode 100644 index 0000000..732e3ca --- /dev/null +++ b/src/render/render2d.cpp @@ -0,0 +1,413 @@ +//----------------------------------------------------------------------------- +// Rendering projections to 2d surfaces: z-sorting, occlusion testing, etc. +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +// FIXME: The export coordinate system has a different handedness than display +// coordinate system; lighting and occlusion calculations are right-handed. +static Vector ProjectPoint3RH(const Camera &camera, Vector p) { + p = p.Plus(camera.offset); + + Vector r; + r.x = p.Dot(camera.projRight); + r.y = p.Dot(camera.projUp); + r.z = p.Dot(camera.projRight.Cross(camera.projUp)); + + double w = 1 + r.z*camera.tangent*camera.scale; + return r.ScaledBy(camera.scale/w); +} + +//----------------------------------------------------------------------------- +// Accumulation of geometry +//----------------------------------------------------------------------------- + +void SurfaceRenderer::DrawLine(const Vector &a, const Vector &b, hStroke hcs) { + edges[hcs].AddEdge(ProjectPoint3RH(camera, a), + ProjectPoint3RH(camera, b)); +} + +void SurfaceRenderer::DrawEdges(const SEdgeList &el, hStroke hcs) { + for(const SEdge &e : el.l) { + edges[hcs].AddEdge(ProjectPoint3RH(camera, e.a), + ProjectPoint3RH(camera, e.b)); + } +} + +bool SurfaceRenderer::DrawBeziers(const SBezierList &bl, hStroke hcs) { + if(!CanOutputCurves()) + return false; + + for(const SBezier &b : bl.l) { + SBezier pb = camera.ProjectBezier(b); + beziers[hcs].l.Add(&pb); + } + return true; +} + +void SurfaceRenderer::DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs drawAs) { + Vector projDir = camera.projRight.Cross(camera.projUp); + for(const SOutline &o : ol.l) { + if(drawAs == DrawOutlinesAs::EMPHASIZED_AND_CONTOUR && + !(o.IsVisible(projDir) || o.tag != 0)) + continue; + if(drawAs == DrawOutlinesAs::EMPHASIZED_WITHOUT_CONTOUR && + !(!o.IsVisible(projDir) && o.tag != 0)) + continue; + if(drawAs == DrawOutlinesAs::CONTOUR_ONLY && + !(o.IsVisible(projDir))) + continue; + + edges[hcs].AddEdge(ProjectPoint3RH(camera, o.a), + ProjectPoint3RH(camera, o.b)); + } +} + +void SurfaceRenderer::DrawVectorText(const std::string &text, double height, + const Vector &o, const Vector &u, const Vector &v, + hStroke hcs) { + auto traceEdge = [&](Vector a, Vector b) { + edges[hcs].AddEdge(ProjectPoint3RH(camera, a), + ProjectPoint3RH(camera, b)); + }; + VectorFont::Builtin()->Trace(height, o, u, v, text, traceEdge, camera); +} + +void SurfaceRenderer::DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + hFill hcf) { + Fill *fill = fills.FindById(hcf); + ssassert(fill->layer == Layer::NORMAL || + fill->layer == Layer::DEPTH_ONLY || + fill->layer == Layer::FRONT || + fill->layer == Layer::BACK, "Unexpected mesh layer"); + + Vector zOffset = {}; + if(fill->layer == Layer::BACK) { + zOffset.z -= 1e6; + } else if(fill->layer == Layer::FRONT) { + zOffset.z += 1e6; + } + zOffset.z += camera.scale * fill->zIndex; + + STriMeta meta = {}; + if(fill->layer != Layer::DEPTH_ONLY) { + meta.color = fill->color; + } + Vector ta = ProjectPoint3RH(camera, a).Plus(zOffset), + tb = ProjectPoint3RH(camera, b).Plus(zOffset), + tc = ProjectPoint3RH(camera, c).Plus(zOffset), + td = ProjectPoint3RH(camera, d).Plus(zOffset); + mesh.AddTriangle(meta, tc, tb, ta); + mesh.AddTriangle(meta, ta, td, tc); +} + +void SurfaceRenderer::DrawPoint(const Vector &o, Canvas::hStroke hcs) { + Stroke *stroke = strokes.FindById(hcs); + + Fill fill = {}; + fill.layer = stroke->layer; + fill.zIndex = stroke->zIndex; + fill.color = stroke->color; + hFill hcf = GetFill(fill); + + Vector u = camera.projRight.ScaledBy(stroke->width/2.0/camera.scale), + v = camera.projUp.ScaledBy(stroke->width/2.0/camera.scale); + DrawQuad(o.Minus(u).Minus(v), o.Minus(u).Plus(v), + o.Plus(u).Plus(v), o.Plus(u).Minus(v), hcf); +} + +void SurfaceRenderer::DrawPolygon(const SPolygon &p, hFill hcf) { + SMesh m = {}; + p.TriangulateInto(&m); + DrawMesh(m, hcf, {}); + m.Clear(); +} + +void SurfaceRenderer::DrawMesh(const SMesh &m, + hFill hcfFront, hFill hcfBack) { + Fill *fill = fills.FindById(hcfFront); + ssassert(fill->layer == Layer::NORMAL || + fill->layer == Layer::DEPTH_ONLY, "Unexpected mesh layer"); + + Vector l0 = (lighting.lightDirection[0]).WithMagnitude(1), + l1 = (lighting.lightDirection[1]).WithMagnitude(1); + for(STriangle tr : m.l) { + tr.a = ProjectPoint3RH(camera, tr.a); + tr.b = ProjectPoint3RH(camera, tr.b); + tr.c = ProjectPoint3RH(camera, tr.c); + + if(CanOutputTriangles() && fill->layer == Layer::NORMAL) { + if(fill->color.IsEmpty()) { + // Compute lighting, since we're going to draw the shaded triangles. + Vector n = tr.Normal().WithMagnitude(1); + double intensity = lighting.ambientIntensity + + max(0.0, (lighting.lightIntensity[0])*(n.Dot(l0))) + + max(0.0, (lighting.lightIntensity[1])*(n.Dot(l1))); + double r = min(1.0, tr.meta.color.redF() * intensity), + g = min(1.0, tr.meta.color.greenF() * intensity), + b = min(1.0, tr.meta.color.blueF() * intensity); + tr.meta.color = RGBf(r, g, b); + } else { + // We're going to draw this triangle, but it's not shaded. + tr.meta.color = fill->color; + } + } else { + // This triangle is just for occlusion testing. + tr.meta.color = {}; + } + mesh.AddTriangle(&tr); + } +} + +void SurfaceRenderer::DrawFaces(const SMesh &m, const std::vector &faces, hFill hcf) { + Fill *fill = fills.FindById(hcf); + ssassert(fill->layer == Layer::NORMAL || + fill->layer == Layer::DEPTH_ONLY, "Unexpected mesh layer"); + + Vector zOffset = {}; + zOffset.z += camera.scale * fill->zIndex; + + size_t facesSize = faces.size(); + for(STriangle tr : m.l) { + uint32_t face = tr.meta.face; + for(size_t j = 0; j < facesSize; j++) { + if(faces[j] != face) continue; + if(!fill->color.IsEmpty()) { + tr.meta.color = fill->color; + } + mesh.AddTriangle(tr.meta, + ProjectPoint3RH(camera, tr.a).Plus(zOffset), + ProjectPoint3RH(camera, tr.b).Plus(zOffset), + ProjectPoint3RH(camera, tr.c).Plus(zOffset)); + break; + } + } +} + +void SurfaceRenderer::DrawPixmap(std::shared_ptr pm, + const Vector &o, const Vector &u, const Vector &v, + const Point2d &ta, const Point2d &tb, hFill hcf) { + dbp("Not implemented"); +} + +void SurfaceRenderer::InvalidatePixmap(std::shared_ptr pm) { + dbp("Not implemented"); +} + +//----------------------------------------------------------------------------- +// Processing of geometry +//----------------------------------------------------------------------------- + +void SurfaceRenderer::CalculateBBox() { + bbox.minp = Vector::From(VERY_POSITIVE, VERY_POSITIVE, VERY_POSITIVE); + bbox.maxp = Vector::From(VERY_NEGATIVE, VERY_NEGATIVE, VERY_NEGATIVE); + + for(auto &it : edges) { + SEdgeList &el = it.second; + for(SEdge &e : el.l) { + bbox.Include(e.a); + bbox.Include(e.b); + } + } + + for(auto &it : beziers) { + SBezierList &bl = it.second; + for(SBezier &b : bl.l) { + for(int i = 0; i <= b.deg; i++) { + bbox.Include(b.ctrl[i]); + } + } + } + + for(STriangle &tr : mesh.l) { + for(int i = 0; i < 3; i++) { + bbox.Include(tr.vertices[i]); + } + } +} + + +void SurfaceRenderer::ConvertBeziersToEdges() { + for(auto &it : beziers) { + hStroke hcs = it.first; + SBezierList &bl = it.second; + + SEdgeList &el = edges[hcs]; + for(const SBezier &b : bl.l) { + if(b.deg == 1) { + el.AddEdge(b.ctrl[0], b.ctrl[1]); + } else { + List lv = {}; + b.MakePwlInto(&lv, chordTolerance); + for(int i = 1; i < lv.n; i++) { + el.AddEdge(lv[i-1], lv[i]); + } + lv.Clear(); + } + } + bl.l.Clear(); + } + beziers.clear(); +} + +void SurfaceRenderer::CullOccludedStrokes() { + // Perform occlusion testing, if necessary. + if(mesh.l.IsEmpty()) + return; + + // We can't perform hidden line removal on exact curves. + ConvertBeziersToEdges(); + + // Remove hidden lines (on NORMAL layers), or remove visible lines (on OCCLUDED layers). + SKdNode *root = SKdNode::From(&mesh); + root->ClearTags(); + + int cnt = 1234; + for(auto &eit : edges) { + hStroke hcs = eit.first; + SEdgeList &el = eit.second; + + Stroke *stroke = strokes.FindById(hcs); + if(stroke->layer != Layer::NORMAL && + stroke->layer != Layer::OCCLUDED) continue; + + SEdgeList nel = {}; + for(const SEdge &e : el.l) { + SEdgeList oel = {}; + oel.AddEdge(e.a, e.b); + root->OcclusionTestLine(e, &oel, cnt); + + if(stroke->layer == Layer::OCCLUDED) { + for(SEdge &oe : oel.l) { + oe.tag = !oe.tag; + } + } + oel.l.RemoveTagged(); + + oel.MergeCollinearSegments(e.a, e.b); + for(const SEdge &oe : oel.l) { + nel.AddEdge(oe.a, oe.b); + } + + oel.Clear(); + cnt++; + } + + el.l.Clear(); + el.l = nel.l; + } +} + +void SurfaceRenderer::OutputInPaintOrder() { + // Sort our strokes in paint order. + std::vector> paintOrder; + paintOrder.emplace_back(Layer::NORMAL, 0); // mesh + for(const Stroke &cs : strokes) { + paintOrder.emplace_back(cs.layer, cs.zIndex); + } + + const Layer stackup[] = { + Layer::BACK, Layer::NORMAL, Layer::DEPTH_ONLY, Layer::OCCLUDED, Layer::FRONT + }; + std::sort(paintOrder.begin(), paintOrder.end(), + [&](std::pair a, std::pair b) { + Layer aLayer = a.first, + bLayer = b.first; + int aZIndex = a.second, + bZIndex = b.second; + + size_t aLayerIndex = + std::find(std::begin(stackup), std::end(stackup), aLayer) - std::begin(stackup); + size_t bLayerIndex = + std::find(std::begin(stackup), std::end(stackup), bLayer) - std::begin(stackup); + if(aLayerIndex == bLayerIndex) { + return aZIndex < bZIndex; + } else { + return aLayerIndex < bLayerIndex; + } + }); + + auto last = std::unique(paintOrder.begin(), paintOrder.end()); + paintOrder.erase(last, paintOrder.end()); + + // Output geometry in paint order. + OutputStart(); + for(auto &it : paintOrder) { + Layer layer = it.first; + int zIndex = it.second; + + if(layer == Layer::NORMAL && zIndex == 0) { + SMesh mp = {}; + SBsp3 *bsp = SBsp3::FromMesh(&mesh); + if(bsp) bsp->GenerateInPaintOrder(&mp); + + for(const STriangle &tr : mp.l) { + // Cull back-facing and invisible triangles. + if(tr.Normal().z < 0) continue; + if(tr.meta.color.IsEmpty()) continue; + OutputTriangle(tr); + } + + mp.Clear(); + } + + for(const auto &eit : edges) { + hStroke hcs = eit.first; + const SEdgeList &el = eit.second; + + Stroke *stroke = strokes.FindById(hcs); + if(stroke->layer != layer || stroke->zIndex != zIndex) continue; + + for(const SEdge &e : el.l) { + OutputBezier(SBezier::From(e.a, e.b), hcs); + } + } + + for(auto &bit : beziers) { + hStroke hcs = bit.first; + const SBezierList &bl = bit.second; + + Stroke *stroke = strokes.FindById(hcs); + if(stroke->layer != layer || stroke->zIndex != zIndex) continue; + + for(const SBezier &b : bl.l) { + OutputBezier(b, hcs); + } + } + } + OutputEnd(); +} + +void SurfaceRenderer::Clear() { + Canvas::Clear(); + + for(auto &eit : edges) { + SEdgeList &el = eit.second; + el.l.Clear(); + } + edges.clear(); + + for(auto &bit : beziers) { + SBezierList &bl = bit.second; + bl.l.Clear(); + } + beziers.clear(); + + mesh.Clear(); +} + +void SurfaceRenderer::OutputBezierAsNonrationalCubic(const SBezier &b, hStroke hcs) { + // Arbitrary choice of tolerance; make it a little finer than pwl tolerance since + // it should be easier to achieve that with the smooth curves. + SBezierList bl; + b.MakeNonrationalCubicInto(&bl, chordTolerance / 2); + for(const SBezier &cb : bl.l) { + OutputBezier(cb, hcs); + } + bl.Clear(); +} + +} diff --git a/src/render/rendercairo.cpp b/src/render/rendercairo.cpp new file mode 100644 index 0000000..e59de31 --- /dev/null +++ b/src/render/rendercairo.cpp @@ -0,0 +1,173 @@ +//----------------------------------------------------------------------------- +// A rendering backend that draws on a Cairo surface. +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- +#include +#include "solvespace.h" + +namespace SolveSpace { + +void CairoRenderer::Clear() { + SurfaceRenderer::Clear(); + + if(context != NULL) cairo_destroy(context); + context = NULL; +} + +void CairoRenderer::GetIdent(const char **vendor, const char **renderer, const char **version) { + *vendor = "Cairo"; + *renderer = "Cairo"; + *version = cairo_version_string(); +} + +void CairoRenderer::FlushFrame() { + CullOccludedStrokes(); + OutputInPaintOrder(); + + cairo_surface_flush(cairo_get_target(context)); +} + +std::shared_ptr CairoRenderer::ReadFrame() { + ssassert(false, "generic Cairo renderer does not support pixmap readout"); +} + +void CairoRenderer::OutputStart() { + cairo_save(context); + + RgbaColor bgColor = lighting.backgroundColor; + cairo_rectangle(context, 0.0, 0.0, (double)camera.width, (double)camera.height); + cairo_set_source_rgba(context, bgColor.redF(), bgColor.greenF(), bgColor.blueF(), + bgColor.alphaF()); + cairo_fill(context); + + cairo_translate(context, camera.width / 2.0, camera.height / 2.0); + + // Avoid pixel boundaries; when not using antialiasing, we would otherwise + // get numerically unstable output. + cairo_translate(context, 0.1, 0.1); + + cairo_set_line_join(context, CAIRO_LINE_JOIN_ROUND); + cairo_set_line_cap(context, CAIRO_LINE_CAP_ROUND); +} + +void CairoRenderer::OutputEnd() { + FinishPath(); + + cairo_restore(context); +} + +void CairoRenderer::SelectStroke(hStroke hcs) { + if(current.hcs == hcs) return; + FinishPath(); + + Stroke *stroke = strokes.FindById(hcs); + current.hcs = hcs; + + RgbaColor color = stroke->color; + std::vector dashes = StipplePatternDashes(stroke->stipplePattern); + for(double &dash : dashes) { + dash *= stroke->StippleScalePx(camera); + } + cairo_set_line_width(context, stroke->WidthPx(camera)); + cairo_set_dash(context, dashes.data(), (int)dashes.size(), 0); + cairo_set_source_rgba(context, color.redF(), color.greenF(), color.blueF(), + color.alphaF()); + if(antialias) { + cairo_set_antialias(context, CAIRO_ANTIALIAS_GRAY); + } else { + cairo_set_antialias(context, CAIRO_ANTIALIAS_NONE); + } +} + +void CairoRenderer::MoveTo(Vector p) { + Point2d pos; + cairo_get_current_point(context, &pos.x, &pos.y); + if(cairo_has_current_point(context) && pos.Equals(p.ProjectXy())) return; + FinishPath(); + + cairo_move_to(context, p.x, p.y); +} + +void CairoRenderer::FinishPath() { + if(!cairo_has_current_point(context)) return; + + cairo_stroke(context); +} + +void CairoRenderer::OutputBezier(const SBezier &b, hStroke hcs) { + SelectStroke(hcs); + + Vector c, n = Vector::From(0, 0, 1); + double r; + if(b.deg == 1) { + MoveTo(b.ctrl[0]); + cairo_line_to(context, + b.ctrl[1].x, b.ctrl[1].y); + } else if(b.IsCircle(n, &c, &r)) { + MoveTo(b.ctrl[0]); + double theta0 = atan2(b.ctrl[0].y - c.y, b.ctrl[0].x - c.x), + theta1 = atan2(b.ctrl[2].y - c.y, b.ctrl[2].x - c.x), + dtheta = WRAP_SYMMETRIC(theta1 - theta0, 2*PI); + if(dtheta > 0) { + cairo_arc(context, + c.x, c.y, r, theta0, theta1); + } else { + cairo_arc_negative(context, + c.x, c.y, r, theta0, theta1); + } + } else if(b.deg == 3 && !b.IsRational()) { + MoveTo(b.ctrl[0]); + cairo_curve_to(context, + b.ctrl[1].x, b.ctrl[1].y, + b.ctrl[2].x, b.ctrl[2].y, + b.ctrl[3].x, b.ctrl[3].y); + } else { + OutputBezierAsNonrationalCubic(b, hcs); + } +} + +void CairoRenderer::OutputTriangle(const STriangle &tr) { + FinishPath(); + current.hcs = {}; + + RgbaColor color = tr.meta.color; + cairo_set_source_rgba(context, color.redF(), color.greenF(), color.blueF(), + color.alphaF()); + cairo_set_antialias(context, CAIRO_ANTIALIAS_NONE); + cairo_move_to(context, tr.a.x, tr.a.y); + cairo_line_to(context, tr.b.x, tr.b.y); + cairo_line_to(context, tr.c.x, tr.c.y); + cairo_fill(context); +} + +void CairoPixmapRenderer::Init() { + Clear(); + + pixmap = std::make_shared(); + pixmap->format = Pixmap::Format::BGRA; + pixmap->width = (size_t)camera.width; + pixmap->height = (size_t)camera.height; + pixmap->stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, (int)camera.width); + pixmap->data = std::vector(pixmap->stride * pixmap->height); + surface = + cairo_image_surface_create_for_data(&pixmap->data[0], CAIRO_FORMAT_RGB24, + pixmap->width, pixmap->height, + pixmap->stride); + context = cairo_create(surface); +} + +void CairoPixmapRenderer::Clear() { + CairoRenderer::Clear(); + + if(surface != NULL) cairo_surface_destroy(surface); + surface = NULL; +} + +std::shared_ptr CairoPixmapRenderer::ReadFrame() { + std::shared_ptr result = pixmap->Copy(); + result->ConvertTo(Pixmap::Format::RGBA); + return result; +} + +} diff --git a/src/render/rendergl1.cpp b/src/render/rendergl1.cpp new file mode 100644 index 0000000..a915c2b --- /dev/null +++ b/src/render/rendergl1.cpp @@ -0,0 +1,857 @@ +//----------------------------------------------------------------------------- +// OpenGL 1 based rendering interface. +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- +#include "solvespace.h" + +#ifdef WIN32 +// Include after solvespace.h to avoid identifier clashes. +# include // required by GL headers +#endif +#ifdef __APPLE__ +# include +# include +#else +# include +# include +#endif + +namespace SolveSpace { + +//----------------------------------------------------------------------------- +// Checks for buggy OpenGL renderers +//----------------------------------------------------------------------------- + +// Intel GPUs with Mesa on *nix render thin lines poorly. +static bool HasIntelThinLineQuirk() +{ + static bool quirkChecked, quirkEnabled; + if(!quirkChecked) { + const char *ident = (const char*)glGetString(GL_VENDOR); + if(ident != NULL) { + quirkChecked = true; + quirkEnabled = !strcmp(ident, "Intel Open Source Technology Center"); + } + } + return quirkEnabled; +} + +// The default Windows GL renderer really does implement GL 1.1, +// and cannot handle non-power-of-2 textures, which is legal. +static bool HasGl1V1Quirk() +{ + static bool quirkChecked, quirkEnabled; + if(!quirkChecked) { + const char *ident = (const char*)glGetString(GL_VERSION); + if(ident != NULL) { + quirkChecked = true; + quirkEnabled = !strcmp(ident, "1.1.0"); + } + } + return quirkEnabled; +} + +//----------------------------------------------------------------------------- +// Thin wrappers around OpenGL functions to fix bugs, adapt them to our +// data structures, etc. +//----------------------------------------------------------------------------- + +static inline void ssglNormal3v(Vector n) { + glNormal3d(n.x, n.y, n.z); +} + +static inline void ssglVertex3v(Vector v) { + glVertex3d(v.x, v.y, v.z); +} + +void ssglLineWidth(double width) { + if(HasIntelThinLineQuirk() && width < 1.6) + width = 1.6; + + glLineWidth((GLfloat)width); +} + +static inline void ssglColorRGBA(RgbaColor color) { + glColor4d(color.redF(), color.greenF(), color.blueF(), color.alphaF()); +} + +static inline void ssglMaterialRGBA(GLenum side, RgbaColor color) { + GLfloat mpb[] = { color.redF(), color.greenF(), color.blueF(), color.alphaF() }; + glMaterialfv(side, GL_AMBIENT_AND_DIFFUSE, mpb); +} + +static void ssglDepthRange(Canvas::Layer layer, int zIndex) { + switch(layer) { + case Canvas::Layer::NORMAL: + case Canvas::Layer::FRONT: + case Canvas::Layer::BACK: + glDepthFunc(GL_LEQUAL); + glDepthMask(GL_TRUE); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + break; + + case Canvas::Layer::DEPTH_ONLY: + glDepthFunc(GL_LEQUAL); + glDepthMask(GL_TRUE); + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + break; + + case Canvas::Layer::OCCLUDED: + glDepthFunc(GL_GREATER); + glDepthMask(GL_FALSE); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + break; + } + + switch(layer) { + case Canvas::Layer::FRONT: + glDepthRange(0.0, 0.0); + break; + + case Canvas::Layer::BACK: + glDepthRange(1.0, 1.0); + break; + + case Canvas::Layer::NORMAL: + case Canvas::Layer::DEPTH_ONLY: + case Canvas::Layer::OCCLUDED: + // The size of this step depends on the resolution of the Z buffer; for + // a 16-bit buffer, this should be fine. + double offset = 1.0 / (65535 * 0.8) * zIndex; + glDepthRange(0.1 - offset, 1.0 - offset); + break; + } +} + +static void ssglFillPattern(Canvas::FillPattern pattern) { + static bool Init; + static GLubyte MaskA[(32*32)/8]; + static GLubyte MaskB[(32*32)/8]; + if(!Init) { + int x, y; + for(x = 0; x < 32; x++) { + for(y = 0; y < 32; y++) { + int i = y*4 + x/8, b = x % 8; + int ym = y % 4, xm = x % 4; + for(int k = 0; k < 2; k++) { + if(xm >= 1 && xm <= 2 && ym >= 1 && ym <= 2) { + (k == 0 ? MaskB : MaskA)[i] |= (0x80 >> b); + } + ym = (ym + 2) % 4; xm = (xm + 2) % 4; + } + } + } + Init = true; + } + + switch(pattern) { + case Canvas::FillPattern::SOLID: + glDisable(GL_POLYGON_STIPPLE); + break; + + case Canvas::FillPattern::CHECKERED_A: + glEnable(GL_POLYGON_STIPPLE); + glPolygonStipple(MaskA); + break; + + case Canvas::FillPattern::CHECKERED_B: + glEnable(GL_POLYGON_STIPPLE); + glPolygonStipple(MaskB); + break; + } +} + +//----------------------------------------------------------------------------- +// OpenGL 1 / compatibility profile based renderer +//----------------------------------------------------------------------------- + +class OpenGl1Renderer final : public ViewportCanvas { +public: + Camera camera; + Lighting lighting; + // Cached OpenGL state. + struct { + bool drawing; + GLenum mode; + hStroke hcs; + Stroke *stroke; + hFill hcf; + Fill *fill; + std::weak_ptr texture; + } current; + + // List-initialize current to work around MSVC bug 746973. + OpenGl1Renderer() : camera(), lighting(), current({}) {} + + const Camera &GetCamera() const override { return camera; } + + void DrawLine(const Vector &a, const Vector &b, hStroke hcs) override; + void DrawEdges(const SEdgeList &el, hStroke hcs) override; + bool DrawBeziers(const SBezierList &bl, hStroke hcs) override { return false; } + void DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs drawAs) override; + void DrawVectorText(const std::string &text, double height, + const Vector &o, const Vector &u, const Vector &v, + hStroke hcs) override; + + void DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + hFill hcf) override; + void DrawPoint(const Vector &o, hStroke hcs) override; + void DrawPolygon(const SPolygon &p, hFill hcf) override; + void DrawMesh(const SMesh &m, hFill hcfFront, hFill hcfBack) override; + void DrawFaces(const SMesh &m, const std::vector &faces, hFill hcf) override; + void DrawPixmap(std::shared_ptr pm, + const Vector &o, const Vector &u, const Vector &v, + const Point2d &ta, const Point2d &tb, hFill hcf) override; + void InvalidatePixmap(std::shared_ptr pm) override; + + void SelectPrimitive(unsigned mode); + void UnSelectPrimitive(); + Stroke *SelectStroke(hStroke hcs); + Fill *SelectFill(hFill hcf); + void SelectTexture(std::shared_ptr pm); + void DoFatLineEndcap(const Vector &p, const Vector &u, const Vector &v); + void DoFatLine(const Vector &a, const Vector &b, double width); + void DoLine(const Vector &a, const Vector &b, hStroke hcs); + void DoPoint(Vector p, double radius); + void DoStippledLine(const Vector &a, const Vector &b, hStroke hcs, double phase = 0.0); + + void UpdateProjection(); + void SetCamera(const Camera &camera) override; + void SetLighting(const Lighting &lighting) override; + + void StartFrame() override; + void FlushFrame() override; + void FinishFrame() override; + std::shared_ptr ReadFrame() override; + + void GetIdent(const char **vendor, const char **renderer, const char **version) override; +}; + +//----------------------------------------------------------------------------- +// A simple OpenGL state tracker to group consecutive draw calls. +//----------------------------------------------------------------------------- + +void OpenGl1Renderer::SelectPrimitive(GLenum mode) { + if(current.drawing && current.mode == mode) { + return; + } else if(current.drawing) { + glEnd(); + } + glBegin(mode); + current.drawing = true; + current.mode = mode; +} + +void OpenGl1Renderer::UnSelectPrimitive() { + if(!current.drawing) return; + glEnd(); + current.drawing = false; +} + +Canvas::Stroke *OpenGl1Renderer::SelectStroke(hStroke hcs) { + if(current.hcs == hcs) return current.stroke; + + Stroke *stroke = strokes.FindById(hcs); + UnSelectPrimitive(); + ssglColorRGBA(stroke->color); + ssglDepthRange(stroke->layer, stroke->zIndex); + ssglLineWidth(stroke->WidthPx(camera)); + // Fat lines and points are quads affected by glPolygonStipple, so make sure + // they are displayed correctly. + ssglFillPattern(FillPattern::SOLID); + glDisable(GL_TEXTURE_2D); + + current.hcs = hcs; + current.stroke = stroke; + current.hcf = {}; + current.fill = NULL; + current.texture.reset(); + return stroke; +} + +Canvas::Fill *OpenGl1Renderer::SelectFill(hFill hcf) { + if(current.hcf == hcf) return current.fill; + + Fill *fill = fills.FindById(hcf); + UnSelectPrimitive(); + ssglColorRGBA(fill->color); + ssglDepthRange(fill->layer, fill->zIndex); + ssglFillPattern(fill->pattern); + glDisable(GL_TEXTURE_2D); + + current.hcs = {}; + current.stroke = NULL; + current.hcf = hcf; + current.fill = fill; + current.texture.reset(); + return fill; +} + +static int RoundUpToPowerOfTwo(int v) +{ + for(int i = 0; i < 31; i++) { + int vt = (1 << i); + if(vt >= v) { + return vt; + } + } + return 0; +} + +void OpenGl1Renderer::SelectTexture(std::shared_ptr pm) { + if(current.texture.lock() == pm) return; + + glBindTexture(GL_TEXTURE_2D, 1); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); + glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); + + GLenum format = 0; + switch(pm->format) { + case Pixmap::Format::RGBA: format = GL_RGBA; break; + case Pixmap::Format::RGB: format = GL_RGB; break; + case Pixmap::Format::A: format = GL_ALPHA; break; + case Pixmap::Format::BGRA: + case Pixmap::Format::BGR: + ssassert(false, "Unexpected pixmap format"); + } + + if(!HasGl1V1Quirk()) { + glTexImage2D(GL_TEXTURE_2D, 0, format, pm->width, pm->height, 0, + format, GL_UNSIGNED_BYTE, &pm->data[0]); + } else { + GLsizei width = RoundUpToPowerOfTwo(pm->width); + GLsizei height = RoundUpToPowerOfTwo(pm->height); + glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, + format, GL_UNSIGNED_BYTE, 0); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, pm->width, pm->height, + format, GL_UNSIGNED_BYTE, &pm->data[0]); + } + + glEnable(GL_TEXTURE_2D); + + current.texture = pm; +} + +//----------------------------------------------------------------------------- +// OpenGL's GL_LINES mode does not work on lines thicker than about 3 px, +// so we have to draw thicker lines using triangles. +//----------------------------------------------------------------------------- + +void OpenGl1Renderer::DoFatLineEndcap(const Vector &p, const Vector &u, const Vector &v) { + // A table of cos and sin of (pi*i/10 + pi/2), as i goes from 0 to 10 + static const double Circle[11][2] = { + { 0.0000, 1.0000 }, + { -0.3090, 0.9511 }, + { -0.5878, 0.8090 }, + { -0.8090, 0.5878 }, + { -0.9511, 0.3090 }, + { -1.0000, 0.0000 }, + { -0.9511, -0.3090 }, + { -0.8090, -0.5878 }, + { -0.5878, -0.8090 }, + { -0.3090, -0.9511 }, + { 0.0000, -1.0000 }, + }; + + SelectPrimitive(GL_TRIANGLE_FAN); + for(auto pc : Circle) { + double c = pc[0], s = pc[1]; + ssglVertex3v(p.Plus(u.ScaledBy(c)).Plus(v.ScaledBy(s))); + } + UnSelectPrimitive(); +} + +void OpenGl1Renderer::DoFatLine(const Vector &a, const Vector &b, double width) { + // The half-width of the line we're drawing. + double hw = width / 2; + Vector ab = b.Minus(a); + Vector gn = (camera.projRight).Cross(camera.projUp); + Vector abn = (ab.Cross(gn)).WithMagnitude(1); + abn = abn.Minus(gn.ScaledBy(gn.Dot(abn))); + // So now abn is normal to the projection of ab into the screen, so the + // line will always have constant thickness as the view is rotated. + + abn = abn.WithMagnitude(hw); + ab = gn.Cross(abn); + ab = ab. WithMagnitude(hw); + + // The body of a line is a quad + SelectPrimitive(GL_QUADS); + ssglVertex3v(a.Plus (abn)); + ssglVertex3v(b.Plus (abn)); + ssglVertex3v(b.Minus(abn)); + ssglVertex3v(a.Minus(abn)); + // And the line has two semi-circular end caps. + DoFatLineEndcap(a, ab, abn); + DoFatLineEndcap(b, ab.ScaledBy(-1), abn); +} + +void OpenGl1Renderer::DoLine(const Vector &a, const Vector &b, hStroke hcs) { + if(a.Equals(b)) return; + + Stroke *stroke = SelectStroke(hcs); + if(stroke->WidthPx(camera) <= 3.0) { + SelectPrimitive(GL_LINES); + ssglVertex3v(a); + ssglVertex3v(b); + } else { + DoFatLine(a, b, stroke->WidthPx(camera) / camera.scale); + } +} + +void OpenGl1Renderer::DoPoint(Vector p, double d) { + if(d <= 3.0) { + Vector u = camera.projRight.WithMagnitude(d / 2.0 / camera.scale); + + SelectPrimitive(GL_LINES); + ssglVertex3v(p.Minus(u)); + ssglVertex3v(p.Plus(u)); + } else { + Vector u = camera.projRight.WithMagnitude(d / 2.0 / camera.scale); + Vector v = camera.projUp.WithMagnitude(d / 2.0 / camera.scale); + + DoFatLineEndcap(p, u, v); + DoFatLineEndcap(p, u.ScaledBy(-1.0), v); + } +} + +void OpenGl1Renderer::DoStippledLine(const Vector &a, const Vector &b, hStroke hcs, double phase) { + Stroke *stroke = SelectStroke(hcs); + + if(stroke->stipplePattern == StipplePattern::CONTINUOUS) { + DoLine(a, b, hcs); + return; + } + + double scale = stroke->StippleScaleMm(camera); + const std::vector &dashes = StipplePatternDashes(stroke->stipplePattern); + double length = StipplePatternLength(stroke->stipplePattern) * scale; + + phase -= floor(phase / length) * length; + + double curPhase = 0.0; + size_t curDash; + for(curDash = 0; curDash < dashes.size(); curDash++) { + curPhase += dashes[curDash] * scale; + if(phase < curPhase) break; + } + + Vector dir = b.Minus(a); + double len = dir.Magnitude(); + dir = dir.WithMagnitude(1.0); + + double cur = 0.0; + Vector curPos = a; + double width = stroke->WidthMm(camera); + + double curDashLen = (curPhase - phase) / scale; + while(cur < len) { + double next = std::min(len, cur + curDashLen * scale); + Vector nextPos = curPos.Plus(dir.ScaledBy(next - cur)); + if(curDash % 2 == 0) { + if(curDashLen <= LENGTH_EPS) { + DoPoint(curPos, width); + } else { + DoLine(curPos, nextPos, hcs); + } + } + cur = next; + curPos = nextPos; + curDash++; + curDashLen = dashes[curDash % dashes.size()]; + } +} + +//----------------------------------------------------------------------------- +// A canvas implemented using OpenGL 3 immediate mode. +//----------------------------------------------------------------------------- + +void OpenGl1Renderer::DrawLine(const Vector &a, const Vector &b, hStroke hcs) { + DoStippledLine(a, b, hcs); +} + +void OpenGl1Renderer::DrawEdges(const SEdgeList &el, hStroke hcs) { + double phase = 0.0; + for(const SEdge *e = el.l.First(); e; e = el.l.NextAfter(e)) { + DoStippledLine(e->a, e->b, hcs, phase); + phase += e->a.Minus(e->b).Magnitude(); + } +} + +void OpenGl1Renderer::DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs drawAs) { + Vector projDir = camera.projRight.Cross(camera.projUp); + double phase = 0.0; + switch(drawAs) { + case DrawOutlinesAs::EMPHASIZED_AND_CONTOUR: + for(const SOutline &o : ol.l) { + if(o.IsVisible(projDir) || o.tag != 0) { + DoStippledLine(o.a, o.b, hcs, phase); + } + phase += o.a.Minus(o.b).Magnitude(); + } + break; + + case DrawOutlinesAs::EMPHASIZED_WITHOUT_CONTOUR: + for(const SOutline &o : ol.l) { + if(!o.IsVisible(projDir) && o.tag != 0) { + DoStippledLine(o.a, o.b, hcs, phase); + } + phase += o.a.Minus(o.b).Magnitude(); + } + break; + + case DrawOutlinesAs::CONTOUR_ONLY: + for(const SOutline &o : ol.l) { + if(o.IsVisible(projDir)) { + DoStippledLine(o.a, o.b, hcs, phase); + } + phase += o.a.Minus(o.b).Magnitude(); + } + break; + } +} + +void OpenGl1Renderer::DrawVectorText(const std::string &text, double height, + const Vector &o, const Vector &u, const Vector &v, + hStroke hcs) { + auto traceEdge = [&](Vector a, Vector b) { DoStippledLine(a, b, hcs); }; + VectorFont::Builtin()->Trace(height, o, u, v, text, traceEdge, camera); +} + +void OpenGl1Renderer::DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + hFill hcf) { + SelectFill(hcf); + SelectPrimitive(GL_QUADS); + ssglVertex3v(a); + ssglVertex3v(b); + ssglVertex3v(c); + ssglVertex3v(d); +} + +void OpenGl1Renderer::DrawPoint(const Vector &o, Canvas::hStroke hcs) { + Stroke *stroke = SelectStroke(hcs); + + Canvas::Fill fill = {}; + fill.layer = stroke->layer; + fill.zIndex = stroke->zIndex; + fill.color = stroke->color; + hFill hcf = GetFill(fill); + + Vector r = camera.projRight.ScaledBy(stroke->width/2.0/camera.scale); + Vector u = camera.projUp.ScaledBy(stroke->width/2.0/camera.scale); + Vector a = o.Plus (r).Plus (u), + b = o.Plus (r).Minus(u), + c = o.Minus(r).Minus(u), + d = o.Minus(r).Plus (u); + DrawQuad(a, b, c, d, hcf); +} + +#ifdef WIN32 +#define SSGL_CALLBACK CALLBACK +#else +#define SSGL_CALLBACK +#endif +typedef void(SSGL_CALLBACK *GLUCallback)(); + +using CombineVec = std::vector>; + +static void SSGL_CALLBACK Vertex(Vector *p) { + ssglVertex3v(*p); +} +static void SSGL_CALLBACK Combine(double coords[3], void *vertexData[4], + float weight[4], void **outData, void *inData) { + Vector *n = new Vector; + n->x = coords[0]; + n->y = coords[1]; + n->z = coords[2]; + + *outData = n; + CombineVec *vec = static_cast(inData); + vec->emplace_back(n); +} +void OpenGl1Renderer::DrawPolygon(const SPolygon &p, hFill hcf) { + UnSelectPrimitive(); + SelectFill(hcf); + + GLUtesselator *gt = gluNewTess(); + gluTessCallback(gt, GLU_TESS_BEGIN, (GLUCallback) glBegin); + gluTessCallback(gt, GLU_TESS_VERTEX, (GLUCallback) Vertex); + gluTessCallback(gt, GLU_TESS_END, (GLUCallback) glEnd); + gluTessCallback(gt, GLU_TESS_COMBINE_DATA, (GLUCallback) Combine); + + gluTessProperty(gt, GLU_TESS_WINDING_RULE, GLU_TESS_WINDING_ODD); + + ssglNormal3v(p.normal); + gluTessNormal(gt, p.normal.x, p.normal.y, p.normal.z); + + CombineVec vecs; + gluTessBeginPolygon(gt, &vecs); + for(const SContour &sc : p.l) { + gluTessBeginContour(gt); + for(const SPoint &sp : sc.l) { + double ap[3] = { sp.p.x, sp.p.y, sp.p.z }; + gluTessVertex(gt, ap, (GLvoid *) &sp.p); + } + gluTessEndContour(gt); + } + gluTessEndPolygon(gt); + + gluDeleteTess(gt); +} + +void OpenGl1Renderer::DrawMesh(const SMesh &m, hFill hcfFront, hFill hcfBack) { + UnSelectPrimitive(); + + RgbaColor frontColor = {}, + backColor = {}; + + Fill *frontFill = SelectFill(hcfFront); + frontColor = frontFill->color; + + ssglMaterialRGBA(GL_FRONT, frontFill->color); + if(hcfBack.v != 0) { + Fill *backFill = fills.FindById(hcfBack); + backColor = backFill->color; + ssassert(frontFill->layer == backFill->layer && + frontFill->zIndex == backFill->zIndex, + "frontFill and backFill should belong to the same depth range"); + ssassert(frontFill->pattern == backFill->pattern, + "frontFill and backFill should have the same pattern"); + glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 1); + ssglMaterialRGBA(GL_BACK, backFill->color); + } else { + glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 0); + } + + RgbaColor triangleColor = {}; + glEnable(GL_LIGHTING); + glBegin(GL_TRIANGLES); + for(const STriangle &tr : m.l) { + if(frontColor.IsEmpty() || backColor.IsEmpty()) { + if(triangleColor.IsEmpty() || !triangleColor.Equals(tr.meta.color)) { + triangleColor = tr.meta.color; + if(frontColor.IsEmpty()) { + ssglMaterialRGBA(GL_FRONT, triangleColor); + } + if(backColor.IsEmpty()) { + ssglMaterialRGBA(GL_BACK, triangleColor); + } + } + } + + if(tr.an.EqualsExactly(Vector::From(0, 0, 0))) { + // Compute the normal from the vertices + ssglNormal3v(tr.Normal()); + ssglVertex3v(tr.a); + ssglVertex3v(tr.b); + ssglVertex3v(tr.c); + } else { + // Use the exact normals that are specified + ssglNormal3v(tr.an); + ssglVertex3v(tr.a); + ssglNormal3v(tr.bn); + ssglVertex3v(tr.b); + ssglNormal3v(tr.cn); + ssglVertex3v(tr.c); + } + } + glEnd(); + glDisable(GL_LIGHTING); +} + +void OpenGl1Renderer::DrawFaces(const SMesh &m, const std::vector &faces, hFill hcf) { + SelectFill(hcf); + SelectPrimitive(GL_TRIANGLES); + size_t facesSize = faces.size(); + for(const STriangle &tr : m.l) { + uint32_t face = tr.meta.face; + for(size_t j = 0; j < facesSize; j++) { + if(faces[j] != face) continue; + ssglVertex3v(tr.a); + ssglVertex3v(tr.b); + ssglVertex3v(tr.c); + break; + } + } +} + +void OpenGl1Renderer::DrawPixmap(std::shared_ptr pm, + const Vector &o, const Vector &u, const Vector &v, + const Point2d &ta, const Point2d &tb, hFill hcf) { + double xfactor = 1.0, + yfactor = 1.0; + if(HasGl1V1Quirk()) { + xfactor = (double)pm->width / RoundUpToPowerOfTwo(pm->width); + yfactor = (double)pm->height / RoundUpToPowerOfTwo(pm->height); + } + + UnSelectPrimitive(); + SelectFill(hcf); + SelectTexture(pm); + SelectPrimitive(GL_QUADS); + glTexCoord2d(ta.x * xfactor, ta.y * yfactor); + ssglVertex3v(o); + glTexCoord2d(ta.x * xfactor, tb.y * yfactor); + ssglVertex3v(o.Plus(v)); + glTexCoord2d(tb.x * xfactor, tb.y * yfactor); + ssglVertex3v(o.Plus(u).Plus(v)); + glTexCoord2d(tb.x * xfactor, ta.y * yfactor); + ssglVertex3v(o.Plus(u)); +} + +void OpenGl1Renderer::InvalidatePixmap(std::shared_ptr pm) { + if(current.texture.lock() == pm) { + current.texture.reset(); + } +} + +void OpenGl1Renderer::UpdateProjection() { + UnSelectPrimitive(); + + glViewport(0, 0, + (GLsizei)(camera.width * camera.pixelRatio), + (GLsizei)(camera.height * camera.pixelRatio)); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + + glScaled(camera.scale * 2.0 / camera.width, + camera.scale * 2.0 / camera.height, + camera.scale * 1.0 / 30000); + + double mat[16]; + // Last thing before display is to apply the perspective + double clp = camera.tangent * camera.scale; + MakeMatrix(mat, 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, clp, 1); + glMultMatrixd(mat); + + // Before that, we apply the rotation + Vector projRight = camera.projRight, + projUp = camera.projUp, + n = camera.projUp.Cross(camera.projRight); + MakeMatrix(mat, projRight.x, projRight.y, projRight.z, 0, + projUp.x, projUp.y, projUp.z, 0, + n.x, n.y, n.z, 0, + 0, 0, 0, 1); + glMultMatrixd(mat); + + // And before that, the translation + Vector offset = camera.offset; + MakeMatrix(mat, 1, 0, 0, offset.x, + 0, 1, 0, offset.y, + 0, 0, 1, offset.z, + 0, 0, 0, 1); + glMultMatrixd(mat); + + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + + glClearDepth(1.0); + glClear(GL_DEPTH_BUFFER_BIT); +} + +void OpenGl1Renderer::StartFrame() { + glEnable(GL_NORMALIZE); + + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_BLEND); + + glEnable(GL_LINE_SMOOTH); + glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); + // don't enable GL_POLYGON_SMOOTH; that looks ugly on some graphics cards, + // drawn with leaks in the mesh + + glDepthFunc(GL_LEQUAL); + glEnable(GL_DEPTH_TEST); + + if(EXACT(lighting.lightIntensity[0] != 0.0)) { + glEnable(GL_LIGHT0); + GLfloat f = (GLfloat)lighting.lightIntensity[0]; + GLfloat li0[] = { f, f, f, 1.0f }; + glLightfv(GL_LIGHT0, GL_DIFFUSE, li0); + glLightfv(GL_LIGHT0, GL_SPECULAR, li0); + + Vector ld = camera.VectorFromProjs(lighting.lightDirection[0]); + GLfloat ld0[4] = { (GLfloat)ld.x, (GLfloat)ld.y, (GLfloat)ld.z, 0 }; + glLightfv(GL_LIGHT0, GL_POSITION, ld0); + } + + if(EXACT(lighting.lightIntensity[1] != 0.0)) { + glEnable(GL_LIGHT1); + GLfloat f = (GLfloat)lighting.lightIntensity[1]; + GLfloat li0[] = { f, f, f, 1.0f }; + glLightfv(GL_LIGHT1, GL_DIFFUSE, li0); + glLightfv(GL_LIGHT1, GL_SPECULAR, li0); + + Vector ld = camera.VectorFromProjs(lighting.lightDirection[1]); + GLfloat ld0[4] = { (GLfloat)ld.x, (GLfloat)ld.y, (GLfloat)ld.z, 0 }; + glLightfv(GL_LIGHT1, GL_POSITION, ld0); + } + + if(EXACT(lighting.ambientIntensity != 0.0)) { + GLfloat ambient[4] = { (float)lighting.ambientIntensity, + (float)lighting.ambientIntensity, + (float)lighting.ambientIntensity, 1 }; + glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambient); + } + + glClearColor(lighting.backgroundColor.redF(), lighting.backgroundColor.greenF(), + lighting.backgroundColor.blueF(), lighting.backgroundColor.alphaF()); + glClear(GL_COLOR_BUFFER_BIT); + + glClearDepth(1.0); + glClear(GL_DEPTH_BUFFER_BIT); +} + +void OpenGl1Renderer::FlushFrame() { + UnSelectPrimitive(); + + glFlush(); +} + +void OpenGl1Renderer::FinishFrame() { + glFinish(); + + GLenum error = glGetError(); + if(error != GL_NO_ERROR) { + dbp("glGetError() == 0x%X %s", error, gluErrorString(error)); + } +} + +std::shared_ptr OpenGl1Renderer::ReadFrame() { + int width = (int)(camera.width * camera.pixelRatio); + int height = (int)(camera.height * camera.pixelRatio); + std::shared_ptr pixmap = + Pixmap::Create(Pixmap::Format::RGB, (size_t)width, (size_t)height); + glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, &pixmap->data[0]); + ssassert(glGetError() == GL_NO_ERROR, "Unexpected glReadPixels error"); + return pixmap; +} + +void OpenGl1Renderer::GetIdent(const char **vendor, const char **renderer, const char **version) { + *vendor = (const char *)glGetString(GL_VENDOR); + *renderer = (const char *)glGetString(GL_RENDERER); + *version = (const char *)glGetString(GL_VERSION); +} + +void OpenGl1Renderer::SetCamera(const Camera &c) { + camera = c; + UpdateProjection(); +} + +void OpenGl1Renderer::SetLighting(const Lighting &l) { + lighting = l; +} + +std::shared_ptr CreateRenderer() { + return std::shared_ptr(new OpenGl1Renderer()); +} + +} diff --git a/src/render/rendergl3.cpp b/src/render/rendergl3.cpp new file mode 100644 index 0000000..4816496 --- /dev/null +++ b/src/render/rendergl3.cpp @@ -0,0 +1,1102 @@ +//----------------------------------------------------------------------------- +// OpenGL ES 2.0 and OpenGL 3.0 based rendering interface. +// +// Copyright 2016 Aleksey Egorov +//----------------------------------------------------------------------------- +#include "solvespace.h" +#include "gl3shader.h" + +namespace SolveSpace { + +class TextureCache { +public: + std::map, GLuint, + std::owner_less>> items; + + bool Lookup(std::shared_ptr ptr, GLuint *result) { + auto it = items.find(ptr); + if(it == items.end()) { + GLuint id; + glGenTextures(1, &id); + items[ptr] = id; + *result = id; + return false; + } + + *result = it->second; + return true; + } + + void CleanupUnused() { + for(auto it = items.begin(); it != items.end();) { + if(it->first.expired()) { + glDeleteTextures(1, &it->second); + it = items.erase(it); + continue; + } + it++; + } + } +}; + +// A canvas that uses the core OpenGL 3 profile, for desktop systems. +class OpenGl3Renderer final : public ViewportCanvas { +public: + struct SEdgeListItem { + hStroke h; + SEdgeList lines; + + void Clear() { lines.Clear(); } + }; + + struct SMeshListItem { + hFill h; + SIndexedMesh mesh; + + void Clear() { mesh.Clear(); } + }; + + struct SPointListItem { + hStroke h; + SIndexedMesh points; + + void Clear() { points.Clear(); } + }; + + IdList lines; + IdList meshes; + IdList points; + + TextureCache pixmapCache; + std::shared_ptr masks[3]; + + bool initialized; + StippleAtlas atlas; + MeshRenderer meshRenderer; + IndexedMeshRenderer imeshRenderer; + EdgeRenderer edgeRenderer; + OutlineRenderer outlineRenderer; + + Camera camera; + Lighting lighting; + // Cached OpenGL state. + struct { + hStroke hcs; + Stroke *stroke; + hFill hcf; + Fill *fill; + std::weak_ptr texture; + } current; + const char *vendor = ""; + const char *renderer = ""; + const char *version = ""; + + // List-initialize current to work around MSVC bug 746973. + OpenGl3Renderer() : + lines(), meshes(), points(), pixmapCache(), masks(), + initialized(), atlas(), meshRenderer(), imeshRenderer(), + edgeRenderer(), outlineRenderer(), camera(), lighting(), + current({}) {} + + void Init(); + + const Camera &GetCamera() const override { return camera; } + + void DrawLine(const Vector &a, const Vector &b, hStroke hcs) override; + void DrawEdges(const SEdgeList &el, hStroke hcs) override; + bool DrawBeziers(const SBezierList &bl, hStroke hcs) override { return false; } + void DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs mode) override; + void DrawVectorText(const std::string &text, double height, + const Vector &o, const Vector &u, const Vector &v, + hStroke hcs) override; + + void DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + hFill hcf) override; + void DrawPoint(const Vector &o, hStroke hs) override; + void DrawPolygon(const SPolygon &p, hFill hcf) override; + void DrawMesh(const SMesh &m, hFill hcfFront, hFill hcfBack) override; + void DrawFaces(const SMesh &m, const std::vector &faces, hFill hcf) override; + void DrawPixmap(std::shared_ptr pm, + const Vector &o, const Vector &u, const Vector &v, + const Point2d &ta, const Point2d &tb, hFill hcf) override; + void InvalidatePixmap(std::shared_ptr pm) override; + + std::shared_ptr CreateBatch() override; + + Stroke *SelectStroke(hStroke hcs); + Fill *SelectFill(hFill hcf); + void SelectMask(FillPattern pattern); + void SelectTexture(std::shared_ptr pm); + void DoFatLineEndcap(const Vector &p, const Vector &u, const Vector &v); + void DoFatLine(const Vector &a, const Vector &b, double width); + void DoLine(const Vector &a, const Vector &b, hStroke hcs); + void DoPoint(Vector p, hStroke hs); + void DoStippledLine(const Vector &a, const Vector &b, hStroke hcs); + + void UpdateProjection(); + void SetCamera(const Camera &c) override; + void SetLighting(const Lighting &l) override; + + void StartFrame() override; + void FlushFrame() override; + void FinishFrame() override; + void Clear() override; + std::shared_ptr ReadFrame() override; + + void GetIdent(const char **vendor, const char **renderer, const char **version) override; +}; + +//----------------------------------------------------------------------------- +// Thin wrappers around OpenGL functions to fix bugs, adapt them to our +// data structures, etc. +//----------------------------------------------------------------------------- + +static void ssglDepthRange(Canvas::Layer layer, int zIndex) { + switch(layer) { + case Canvas::Layer::NORMAL: + case Canvas::Layer::FRONT: + case Canvas::Layer::BACK: + glDepthFunc(GL_LEQUAL); + glDepthMask(GL_TRUE); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + break; + + case Canvas::Layer::DEPTH_ONLY: + glDepthFunc(GL_LEQUAL); + glDepthMask(GL_TRUE); + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + break; + + case Canvas::Layer::OCCLUDED: + glDepthFunc(GL_GREATER); + glDepthMask(GL_FALSE); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + break; + } + + switch(layer) { + case Canvas::Layer::FRONT: + glDepthRangef(0.0f, 0.0f); + break; + + case Canvas::Layer::BACK: + glDepthRangef(1.0f, 1.0f); + break; + + case Canvas::Layer::NORMAL: + case Canvas::Layer::DEPTH_ONLY: + case Canvas::Layer::OCCLUDED: + // The size of this step depends on the resolution of the Z buffer; for + // a 16-bit buffer, this should be fine. + double offset = 1.0 / (65535 * 0.8) * zIndex; + glDepthRangef((float)(0.1 - offset), (float)(1.0 - offset)); + break; + } +} + +//----------------------------------------------------------------------------- +// A simple OpenGL state tracker to group consecutive draw calls. +//----------------------------------------------------------------------------- + +Canvas::Stroke *OpenGl3Renderer::SelectStroke(hStroke hcs) { + if(current.hcs == hcs) return current.stroke; + + Stroke *stroke = strokes.FindById(hcs); + ssglDepthRange(stroke->layer, stroke->zIndex); + + current.hcs = hcs; + current.stroke = stroke; + current.hcf = {}; + current.fill = NULL; + current.texture.reset(); + return stroke; +} + +void OpenGl3Renderer::SelectMask(FillPattern pattern) { + if(!masks[0]) { + masks[0] = Pixmap::Create(Pixmap::Format::A, 32, 32); + masks[1] = Pixmap::Create(Pixmap::Format::A, 32, 32); + masks[2] = Pixmap::Create(Pixmap::Format::A, 32, 32); + + for(int x = 0; x < 32; x++) { + for(int y = 0; y < 32; y++) { + masks[0]->data[y * 32 + x] = ((x / 2) % 2 == 0 && (y / 2) % 2 == 0) ? 0xFF : 0x00; + masks[1]->data[y * 32 + x] = ((x / 2) % 2 == 1 && (y / 2) % 2 == 1) ? 0xFF : 0x00; + masks[2]->data[y * 32 + x] = 0xFF; + } + } + } + + switch(pattern) { + case Canvas::FillPattern::SOLID: + SelectTexture(masks[2]); + break; + + case Canvas::FillPattern::CHECKERED_A: + SelectTexture(masks[0]); + break; + + case Canvas::FillPattern::CHECKERED_B: + SelectTexture(masks[1]); + break; + + default: ssassert(false, "Unexpected fill pattern"); + } +} + +Canvas::Fill *OpenGl3Renderer::SelectFill(hFill hcf) { + if(current.hcf == hcf) return current.fill; + + Fill *fill = fills.FindById(hcf); + ssglDepthRange(fill->layer, fill->zIndex); + + current.hcs = {}; + current.stroke = NULL; + current.hcf = hcf; + current.fill = fill; + if(fill->pattern != FillPattern::SOLID) { + SelectMask(fill->pattern); + } else if(fill->texture) { + SelectTexture(fill->texture); + } else { + SelectMask(FillPattern::SOLID); + } + return fill; +} + +static bool IsPowerOfTwo(size_t n) { + return (n & (n - 1)) == 0; +} + +void OpenGl3Renderer::InvalidatePixmap(std::shared_ptr pm) { + GLuint id; + pixmapCache.Lookup(pm, &id); + glBindTexture(GL_TEXTURE_2D, id); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + if(IsPowerOfTwo(pm->width) && IsPowerOfTwo(pm->height)) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + } else { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + + GLenum format = 0; + switch(pm->format) { + case Pixmap::Format::RGBA: format = GL_RGBA; break; + case Pixmap::Format::RGB: format = GL_RGB; break; +#if defined(HAVE_GLES) + case Pixmap::Format::A: format = GL_ALPHA; break; +#else + case Pixmap::Format::A: format = GL_RED; break; +#endif + case Pixmap::Format::BGRA: + case Pixmap::Format::BGR: + ssassert(false, "Unexpected pixmap format"); + } + glTexImage2D(GL_TEXTURE_2D, 0, format, pm->width, pm->height, 0, + format, GL_UNSIGNED_BYTE, &pm->data[0]); +} + +void OpenGl3Renderer::SelectTexture(std::shared_ptr pm) { + if(current.texture.lock() == pm) return; + + GLuint id; + if(!pixmapCache.Lookup(pm, &id)) { + InvalidatePixmap(pm); + } + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, id); + current.texture = pm; +} + +void OpenGl3Renderer::DoLine(const Vector &a, const Vector &b, hStroke hcs) { + SEdgeListItem *eli = lines.FindByIdNoOops(hcs); + if(eli == NULL) { + SEdgeListItem item = {}; + item.h = hcs; + lines.Add(&item); + eli = lines.FindByIdNoOops(hcs); + } + + eli->lines.AddEdge(a, b); +} + +void OpenGl3Renderer::DoPoint(Vector p, hStroke hs) { + SPointListItem *pli = points.FindByIdNoOops(hs); + if(pli == NULL) { + SPointListItem item = {}; + item.h = hs; + points.Add(&item); + pli = points.FindByIdNoOops(hs); + } + + pli->points.AddPoint(p); +} + +void OpenGl3Renderer::DoStippledLine(const Vector &a, const Vector &b, hStroke hcs) { + Stroke *stroke = strokes.FindById(hcs); + if(stroke->stipplePattern != StipplePattern::FREEHAND && + stroke->stipplePattern != StipplePattern::ZIGZAG) + { + DoLine(a, b, hcs); + return; + } + + const char *patternSeq = NULL; + Stroke s = *stroke; + s.stipplePattern = StipplePattern::CONTINUOUS; + hcs = GetStroke(s); + switch(stroke->stipplePattern) { + case StipplePattern::CONTINUOUS: DoLine(a, b, hcs); return; + case StipplePattern::SHORT_DASH: patternSeq = "- "; break; + case StipplePattern::DASH: patternSeq = "- "; break; + case StipplePattern::LONG_DASH: patternSeq = "_ "; break; + case StipplePattern::DASH_DOT: patternSeq = "-."; break; + case StipplePattern::DASH_DOT_DOT: patternSeq = "-.."; break; + case StipplePattern::DOT: patternSeq = "."; break; + case StipplePattern::FREEHAND: patternSeq = "~"; break; + case StipplePattern::ZIGZAG: patternSeq = "~__"; break; + } + + Vector dir = b.Minus(a); + double len = dir.Magnitude(); + dir = dir.WithMagnitude(1.0); + + const char *si = patternSeq; + double end = len; + double ss = stroke->stippleScale / 2.0; + do { + double start = end; + switch(*si) { + case ' ': + end -= 1.0 * ss; + break; + + case '-': + start = max(start - 0.5 * ss, 0.0); + end = max(start - 2.0 * ss, 0.0); + if(start == end) break; + DoLine(a.Plus(dir.ScaledBy(start)), a.Plus(dir.ScaledBy(end)), hcs); + end = max(end - 0.5 * ss, 0.0); + break; + + case '_': + end = max(end - 4.0 * ss, 0.0); + DoLine(a.Plus(dir.ScaledBy(start)), a.Plus(dir.ScaledBy(end)), hcs); + break; + + case '.': + end = max(end - 0.5 * ss, 0.0); + if(end == 0.0) break; + DoPoint(a.Plus(dir.ScaledBy(end)), hcs); + end = max(end - 0.5 * ss, 0.0); + break; + + case '~': { + Vector ab = b.Minus(a); + Vector gn = (camera.projRight).Cross(camera.projUp); + Vector abn = (ab.Cross(gn)).WithMagnitude(1); + abn = abn.Minus(gn.ScaledBy(gn.Dot(abn))); + double pws = 2.0 * stroke->width / camera.scale; + + end = max(end - 0.5 * ss, 0.0); + Vector aa = a.Plus(dir.ScaledBy(start)); + Vector bb = a.Plus(dir.ScaledBy(end)) + .Plus(abn.ScaledBy(pws * (start - end) / (0.5 * ss))); + DoLine(aa, bb, hcs); + if(end == 0.0) break; + + start = end; + end = max(end - 1.0 * ss, 0.0); + aa = a.Plus(dir.ScaledBy(end)) + .Plus(abn.ScaledBy(pws)) + .Minus(abn.ScaledBy(2.0 * pws * (start - end) / ss)); + DoLine(bb, aa, hcs); + if(end == 0.0) break; + + start = end; + end = max(end - 0.5 * ss, 0.0); + bb = a.Plus(dir.ScaledBy(end)) + .Minus(abn.ScaledBy(pws)) + .Plus(abn.ScaledBy(pws * (start - end) / (0.5 * ss))); + DoLine(aa, bb, hcs); + break; + } + + default: ssassert(false, "Unexpected stipple pattern element"); + } + if(*(++si) == 0) si = patternSeq; + } while(end > 0.0); +} + +//----------------------------------------------------------------------------- +// A canvas implemented using OpenGL 3 vertex buffer objects. +//----------------------------------------------------------------------------- + +void OpenGl3Renderer::Init() { + atlas.Init(); + edgeRenderer.Init(&atlas); + outlineRenderer.Init(&atlas); + meshRenderer.Init(); + imeshRenderer.Init(); + + vendor = (const char *)glGetString(GL_VENDOR); + renderer = (const char *)glGetString(GL_RENDERER); + version = (const char *)glGetString(GL_VERSION); + +#if !defined(HAVE_GLES) && !defined(__APPLE__) + GLuint array; + glGenVertexArrays(1, &array); + glBindVertexArray(array); +#endif + UpdateProjection(); +} + +void OpenGl3Renderer::DrawLine(const Vector &a, const Vector &b, hStroke hcs) { + DoStippledLine(a, b, hcs); +} + +void OpenGl3Renderer::DrawEdges(const SEdgeList &el, hStroke hcs) { + for(const SEdge &e : el.l) { + DoStippledLine(e.a, e.b, hcs); + } +} + +void OpenGl3Renderer::DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs mode) { + if(ol.l.IsEmpty()) + return; + + Stroke *stroke = SelectStroke(hcs); + ssassert(stroke->stipplePattern != StipplePattern::ZIGZAG && + stroke->stipplePattern != StipplePattern::FREEHAND, + "ZIGZAG and FREEHAND not supported for outlines"); + + outlineRenderer.SetStroke(*stroke, 1.0 / camera.scale); + outlineRenderer.Draw(ol, mode); +} + +void OpenGl3Renderer::DrawVectorText(const std::string &text, double height, + const Vector &o, const Vector &u, const Vector &v, + hStroke hcs) { + SEdgeListItem *eli = lines.FindByIdNoOops(hcs); + if(eli == NULL) { + SEdgeListItem item = {}; + item.h = hcs; + lines.Add(&item); + eli = lines.FindByIdNoOops(hcs); + } + SEdgeList &lines = eli->lines; + auto traceEdge = [&](Vector a, Vector b) { lines.AddEdge(a, b); }; + VectorFont::Builtin()->Trace(height, o, u, v, text, traceEdge, camera); +} + +void OpenGl3Renderer::DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + hFill hcf) { + SMeshListItem *li = meshes.FindByIdNoOops(hcf); + if(li == NULL) { + SMeshListItem item = {}; + item.h = hcf; + meshes.Add(&item); + li = meshes.FindByIdNoOops(hcf); + } + li->mesh.AddQuad(a, b, c, d); +} + +void OpenGl3Renderer::DrawPoint(const Vector &o, hStroke hs) { + DoPoint(o, hs); +} + +void OpenGl3Renderer::DrawPolygon(const SPolygon &p, hFill hcf) { + Fill *fill = SelectFill(hcf); + + SMesh m = {}; + p.TriangulateInto(&m); + meshRenderer.UseFilled(*fill); + meshRenderer.Draw(m); + m.Clear(); +} + +void OpenGl3Renderer::DrawMesh(const SMesh &m, hFill hcfFront, hFill hcfBack) { + ssassert(false, "Not implemented"); +} + +void OpenGl3Renderer::DrawFaces(const SMesh &m, const std::vector &faces, hFill hcf) { + if(faces.empty()) return; + + Fill *fill = SelectFill(hcf); + + SMesh facesMesh = {}; + for(uint32_t f : faces) { + for(const STriangle &t : m.l) { + if(f != t.meta.face) continue; + facesMesh.l.Add(&t); + } + } + + meshRenderer.UseFilled(*fill); + meshRenderer.Draw(facesMesh); + facesMesh.Clear(); +} + +void OpenGl3Renderer::DrawPixmap(std::shared_ptr pm, + const Vector &o, const Vector &u, const Vector &v, + const Point2d &ta, const Point2d &tb, hFill hcf) { + Fill fill = *fills.FindById(hcf); + fill.texture = pm; + hcf = GetFill(fill); + + SMeshListItem *mli = meshes.FindByIdNoOops(hcf); + if(mli == NULL) { + SMeshListItem item = {}; + item.h = hcf; + meshes.Add(&item); + mli = meshes.FindByIdNoOops(hcf); + } + + mli->mesh.AddPixmap(o, u, v, ta, tb); +} + +void OpenGl3Renderer::UpdateProjection() { + glViewport(0, 0, + (int)(camera.width * camera.pixelRatio), + (int)(camera.height * camera.pixelRatio)); + + double mat1[16]; + double mat2[16]; + + double sx = camera.scale * 2.0 / camera.width; + double sy = camera.scale * 2.0 / camera.height; + double sz = camera.scale * 1.0 / 30000; + + MakeMatrix(mat1, + sx, 0, 0, 0, + 0, sy, 0, 0, + 0, 0, sz, 0, + 0, 0, 0, 1 + ); + + // Last thing before display is to apply the perspective + double clp = camera.tangent * camera.scale; + MakeMatrix(mat2, + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, clp, 1 + ); + + double projection[16]; + MultMatrix(mat1, mat2, projection); + + // Before that, we apply the rotation + Vector u = camera.projRight, + v = camera.projUp, + n = camera.projUp.Cross(camera.projRight); + MakeMatrix(mat1, + u.x, u.y, u.z, 0, + v.x, v.y, v.z, 0, + n.x, n.y, n.z, 0, + 0, 0, 0, 1 + ); + + // And before that, the translation + Vector o = camera.offset; + MakeMatrix(mat2, + 1, 0, 0, o.x, + 0, 1, 0, o.y, + 0, 0, 1, o.z, + 0, 0, 0, 1 + ); + + double modelview[16]; + MultMatrix(mat1, mat2, modelview); + + imeshRenderer.SetProjection(projection); + imeshRenderer.SetModelview(modelview); + meshRenderer.SetProjection(projection); + meshRenderer.SetModelview(modelview); + edgeRenderer.SetProjection(projection); + edgeRenderer.SetModelview(modelview); + outlineRenderer.SetProjection(projection); + outlineRenderer.SetModelview(modelview); + + glClearDepthf(1.0f); + glClear(GL_DEPTH_BUFFER_BIT); +} + +void OpenGl3Renderer::StartFrame() { + if(!initialized) { + Init(); + initialized = true; + } + + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_BLEND); + + glDepthFunc(GL_LEQUAL); + glEnable(GL_DEPTH_TEST); + + RgbaColor backgroundColor = lighting.backgroundColor; + glClearColor(backgroundColor.redF(), backgroundColor.greenF(), + backgroundColor.blueF(), backgroundColor.alphaF()); + glClearDepthf(1.0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + glFrontFace(GL_CW); +} + +void OpenGl3Renderer::FlushFrame() { + for(SMeshListItem &li : meshes) { + Fill *fill = SelectFill(li.h); + + imeshRenderer.UseFilled(*fill); + imeshRenderer.Draw(li.mesh); + li.mesh.Clear(); + } + meshes.Clear(); + + for(SEdgeListItem &eli : lines) { + Stroke *stroke = SelectStroke(eli.h); + + edgeRenderer.SetStroke(*stroke, 1.0 / camera.scale); + edgeRenderer.Draw(eli.lines); + eli.lines.Clear(); + } + lines.Clear(); + + for(SPointListItem &li : points) { + Stroke *stroke = SelectStroke(li.h); + + imeshRenderer.UsePoint(*stroke, 1.0 / camera.scale); + imeshRenderer.Draw(li.points); + li.points.Clear(); + } + points.Clear(); + + glFlush(); +} + +void OpenGl3Renderer::FinishFrame() { + glFinish(); + + GLenum error = glGetError(); + if(error != GL_NO_ERROR) { + dbp("glGetError() == 0x%X", error); + } +} + +void OpenGl3Renderer::Clear() { + ViewportCanvas::Clear(); + pixmapCache.CleanupUnused(); +} + +std::shared_ptr OpenGl3Renderer::ReadFrame() { + int width = (int)(camera.width * camera.pixelRatio); + int height = (int)(camera.height * camera.pixelRatio); + std::shared_ptr pixmap = + Pixmap::Create(Pixmap::Format::RGBA, (size_t)width, (size_t)height); + glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, &pixmap->data[0]); + ssassert(glGetError() == GL_NO_ERROR, "Unexpected glReadPixels error"); + return pixmap; +} + +void OpenGl3Renderer::GetIdent(const char **vendor, const char **renderer, const char **version) { + *vendor = this->vendor; + *renderer = this->renderer; + *version = this->version; +} + +void OpenGl3Renderer::SetCamera(const Camera &c) { + camera = c; + if(initialized) { + UpdateProjection(); + } +} + +void OpenGl3Renderer::SetLighting(const Lighting &l) { + lighting = l; +} + +//----------------------------------------------------------------------------- +// A batch canvas implemented using OpenGL 3 vertex buffer objects. +//----------------------------------------------------------------------------- + +class DrawCall { +public: + virtual Canvas::Layer GetLayer() const = 0; + virtual int GetZIndex() const = 0; + + virtual void Draw(OpenGl3Renderer *renderer) = 0; + virtual void Remove(OpenGl3Renderer *renderer) = 0; +}; + +class EdgeDrawCall final : public DrawCall { +public: + // Key + Canvas::Stroke stroke; + // Data + EdgeRenderer::Handle handle; + + Canvas::Layer GetLayer() const override { return stroke.layer; } + int GetZIndex() const override { return stroke.zIndex; } + + static std::shared_ptr Create(OpenGl3Renderer *renderer, const SEdgeList &el, + Canvas::Stroke *stroke) { + EdgeDrawCall *dc = new EdgeDrawCall(); + dc->stroke = *stroke; + dc->handle = renderer->edgeRenderer.Add(el); + return std::shared_ptr(dc); + } + + void Draw(OpenGl3Renderer *renderer) override { + ssglDepthRange(stroke.layer, stroke.zIndex); + renderer->edgeRenderer.SetStroke(stroke, 1.0 / renderer->camera.scale); + renderer->edgeRenderer.Draw(handle); + } + + void Remove(OpenGl3Renderer *renderer) override { + renderer->edgeRenderer.Remove(handle); + } +}; + +class OutlineDrawCall final : public DrawCall { +public: + // Key + Canvas::Stroke stroke; + // Data + OutlineRenderer::Handle handle; + Canvas::DrawOutlinesAs drawAs; + + Canvas::Layer GetLayer() const override { return stroke.layer; } + int GetZIndex() const override { return stroke.zIndex; } + + static std::shared_ptr Create(OpenGl3Renderer *renderer, const SOutlineList &ol, + Canvas::Stroke *stroke, + Canvas::DrawOutlinesAs drawAs) { + OutlineDrawCall *dc = new OutlineDrawCall(); + dc->stroke = *stroke; + dc->handle = renderer->outlineRenderer.Add(ol); + dc->drawAs = drawAs; + return std::shared_ptr(dc); + } + + void Draw(OpenGl3Renderer *renderer) override { + ssglDepthRange(stroke.layer, stroke.zIndex); + renderer->outlineRenderer.SetStroke(stroke, 1.0 / renderer->camera.scale); + renderer->outlineRenderer.Draw(handle, drawAs); + } + + void Remove(OpenGl3Renderer *renderer) override { + renderer->outlineRenderer.Remove(handle); + } +}; + +class PointDrawCall final : public DrawCall { +public: + // Key + Canvas::Stroke stroke; + // Data + IndexedMeshRenderer::Handle handle; + + Canvas::Layer GetLayer() const override { return stroke.layer; } + int GetZIndex() const override { return stroke.zIndex; } + + static std::shared_ptr Create(OpenGl3Renderer *renderer, const SIndexedMesh &mesh, + Canvas::Stroke *stroke) { + PointDrawCall *dc = new PointDrawCall(); + dc->stroke = *stroke; + dc->handle = renderer->imeshRenderer.Add(mesh); + return std::shared_ptr(dc); + } + + void Draw(OpenGl3Renderer *renderer) override { + ssglDepthRange(stroke.layer, stroke.zIndex); + renderer->imeshRenderer.UsePoint(stroke, 1.0 / renderer->camera.scale); + renderer->imeshRenderer.Draw(handle); + } + + void Remove(OpenGl3Renderer *renderer) override { + renderer->imeshRenderer.Remove(handle); + } +}; + +class PixmapDrawCall final : public DrawCall { +public: + // Key + Canvas::Fill fill; + // Data + IndexedMeshRenderer::Handle handle; + + Canvas::Layer GetLayer() const override { return fill.layer; } + int GetZIndex() const override { return fill.zIndex; } + + static std::shared_ptr Create(OpenGl3Renderer *renderer, const SIndexedMesh &mesh, + Canvas::Fill *fill) { + PixmapDrawCall *dc = new PixmapDrawCall(); + dc->fill = *fill; + dc->handle = renderer->imeshRenderer.Add(mesh); + return std::shared_ptr(dc); + } + + void Draw(OpenGl3Renderer *renderer) override { + ssglDepthRange(fill.layer, fill.zIndex); + if(fill.pattern != Canvas::FillPattern::SOLID) { + renderer->SelectMask(fill.pattern); + } else if(fill.texture) { + renderer->SelectTexture(fill.texture); + } else { + renderer->SelectMask(Canvas::FillPattern::SOLID); + } + renderer->imeshRenderer.UseFilled(fill); + renderer->imeshRenderer.Draw(handle); + } + + void Remove(OpenGl3Renderer *renderer) override { + renderer->imeshRenderer.Remove(handle); + } +}; + +class MeshDrawCall final : public DrawCall { +public: + // Key + Canvas::Fill fillFront; + // Data + MeshRenderer::Handle handle; + Canvas::Fill fillBack; + bool hasFillBack; + bool isShaded; + + Canvas::Layer GetLayer() const override { return fillFront.layer; } + int GetZIndex() const override { return fillFront.zIndex; } + + static std::shared_ptr Create(OpenGl3Renderer *renderer, const SMesh &m, + Canvas::Fill *fillFront, Canvas::Fill *fillBack = NULL, + bool isShaded = false) { + MeshDrawCall *dc = new MeshDrawCall(); + dc->fillFront = *fillFront; + dc->handle = renderer->meshRenderer.Add(m); + dc->fillBack = *fillBack; + dc->isShaded = isShaded; + dc->hasFillBack = (fillBack != NULL); + return std::shared_ptr(dc); + } + + void DrawFace(OpenGl3Renderer *renderer, GLenum cullFace, const Canvas::Fill &fill) { + glCullFace(cullFace); + ssglDepthRange(fill.layer, fill.zIndex); + if(fill.pattern != Canvas::FillPattern::SOLID) { + renderer->SelectMask(fill.pattern); + } else if(fill.texture) { + renderer->SelectTexture(fill.texture); + } else { + renderer->SelectMask(Canvas::FillPattern::SOLID); + } + if(isShaded) { + renderer->meshRenderer.UseShaded(renderer->lighting); + } else { + renderer->meshRenderer.UseFilled(fill); + } + renderer->meshRenderer.Draw(handle, /*useColors=*/fill.color.IsEmpty(), fill.color); + } + + void Draw(OpenGl3Renderer *renderer) override { + glEnable(GL_CULL_FACE); + if(hasFillBack) + DrawFace(renderer, GL_BACK, fillBack); + DrawFace(renderer, GL_FRONT, fillFront); + glDisable(GL_CULL_FACE); + } + + void Remove(OpenGl3Renderer *renderer) override { + renderer->meshRenderer.Remove(handle); + } +}; + +struct CompareDrawCall { + bool operator()(const std::shared_ptr &a, const std::shared_ptr &b) const { + const Canvas::Layer stackup[] = { + Canvas::Layer::BACK, + Canvas::Layer::DEPTH_ONLY, + Canvas::Layer::NORMAL, + Canvas::Layer::OCCLUDED, + Canvas::Layer::FRONT + }; + + int aLayerIndex = + std::find(std::begin(stackup), std::end(stackup), a->GetLayer()) - std::begin(stackup); + int bLayerIndex = + std::find(std::begin(stackup), std::end(stackup), b->GetLayer()) - std::begin(stackup); + if(aLayerIndex == bLayerIndex) { + return a->GetZIndex() < b->GetZIndex(); + } else { + return aLayerIndex < bLayerIndex; + } + } +}; + +class OpenGl3RendererBatch final : public BatchCanvas { +public: + struct EdgeBuffer { + hStroke h; + SEdgeList edges; + + void Clear() { + edges.Clear(); + } + }; + + struct PointBuffer { + hStroke h; + SIndexedMesh points; + + void Clear() { + points.Clear(); + } + }; + + OpenGl3Renderer *renderer; + + IdList edgeBuffer; + IdList pointBuffer; + + std::multiset, CompareDrawCall> drawCalls; + + OpenGl3RendererBatch() : renderer(), edgeBuffer(), pointBuffer() {} + + void DrawLine(const Vector &a, const Vector &b, hStroke hcs) override { + EdgeBuffer *eb = edgeBuffer.FindByIdNoOops(hcs); + if(!eb) { + EdgeBuffer neb = {}; + neb.h = hcs; + edgeBuffer.Add(&neb); + eb = edgeBuffer.FindById(hcs); + } + + eb->edges.AddEdge(a, b); + } + + void DrawEdges(const SEdgeList &el, hStroke hcs) override { + EdgeBuffer *eb = edgeBuffer.FindByIdNoOops(hcs); + if(!eb) { + EdgeBuffer neb = {}; + neb.h = hcs; + edgeBuffer.Add(&neb); + eb = edgeBuffer.FindById(hcs); + } + + for(const SEdge &e : el.l) { + eb->edges.AddEdge(e.a, e.b); + } + } + + bool DrawBeziers(const SBezierList &bl, hStroke hcs) override { + return false; + } + + void DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs drawAs) override { + drawCalls.emplace(OutlineDrawCall::Create(renderer, ol, strokes.FindById(hcs), drawAs)); + } + + void DrawVectorText(const std::string &text, double height, + const Vector &o, const Vector &u, const Vector &v, + hStroke hcs) override { + ssassert(false, "Not implemented"); + } + + void DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + hFill hcf) override { + ssassert(false, "Not implemented"); + } + + void DrawPoint(const Vector &o, hStroke hcs) override { + PointBuffer *pb = pointBuffer.FindByIdNoOops(hcs); + if(!pb) { + PointBuffer npb = {}; + npb.h = hcs; + pointBuffer.Add(&npb); + pb = pointBuffer.FindById(hcs); + } + + pb->points.AddPoint(o); + } + + void DrawPolygon(const SPolygon &p, hFill hcf) override { + SMesh m = {}; + p.TriangulateInto(&m); + drawCalls.emplace(MeshDrawCall::Create(renderer, m, fills.FindById(hcf), + fills.FindById(hcf))); + m.Clear(); + } + + void DrawMesh(const SMesh &m, hFill hcfFront, hFill hcfBack = {}) override { + drawCalls.emplace(MeshDrawCall::Create(renderer, m, fills.FindById(hcfFront), + fills.FindByIdNoOops(hcfBack), + /*isShaded=*/true)); + } + + void DrawFaces(const SMesh &m, const std::vector &faces, hFill hcf) override { + ssassert(false, "Not implemented"); + } + + void DrawPixmap(std::shared_ptr pm, + const Vector &o, const Vector &u, const Vector &v, + const Point2d &ta, const Point2d &tb, hFill hcf) override { + Fill fill = *fills.FindById(hcf); + fill.texture = pm; + hcf = GetFill(fill); + + SIndexedMesh mesh = {}; + mesh.AddPixmap(o, u, v, ta, tb); + drawCalls.emplace(PixmapDrawCall::Create(renderer, mesh, fills.FindByIdNoOops(hcf))); + mesh.Clear(); + } + + void InvalidatePixmap(std::shared_ptr pm) override { + ssassert(false, "Not implemented"); + } + + void Finalize() override { + for(const EdgeBuffer &eb : edgeBuffer) { + drawCalls.emplace(EdgeDrawCall::Create(renderer, eb.edges, strokes.FindById(eb.h))); + } + edgeBuffer.Clear(); + + for(const PointBuffer &pb : pointBuffer) { + drawCalls.emplace(PointDrawCall::Create(renderer, pb.points, strokes.FindById(pb.h))); + } + pointBuffer.Clear(); + } + + void Draw() override { + renderer->current = {}; + + for(std::shared_ptr dc : drawCalls) { + dc->Draw(renderer); + } + } + + void Clear() override { + for(std::shared_ptr dc : drawCalls) { + dc->Remove(renderer); + } + drawCalls.clear(); + } +}; + +//----------------------------------------------------------------------------- +// Factory functions. +//----------------------------------------------------------------------------- + +std::shared_ptr OpenGl3Renderer::CreateBatch() { + OpenGl3RendererBatch *batch = new OpenGl3RendererBatch(); + batch->renderer = this; + return std::shared_ptr(batch); +} + +std::shared_ptr CreateRenderer() { + return std::shared_ptr(new OpenGl3Renderer()); +} + +} diff --git a/src/request.cpp b/src/request.cpp new file mode 100644 index 0000000..cb5e3f3 --- /dev/null +++ b/src/request.cpp @@ -0,0 +1,250 @@ +//----------------------------------------------------------------------------- +// Implementation of our Request class; a request is a user-created thing +// that will generate an entity (line, curve) when the sketch is generated, +// in the same way that other entities are generated automatically, like +// by an extrude or a step and repeat. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +const hRequest Request::HREQUEST_REFERENCE_XY = { 1 }; +const hRequest Request::HREQUEST_REFERENCE_YZ = { 2 }; +const hRequest Request::HREQUEST_REFERENCE_ZX = { 3 }; + +struct EntReqMapping { + Request::Type reqType; + Entity::Type entType; + int points; + bool useExtraPoints; + bool hasNormal; + bool hasDistance; +}; +static const EntReqMapping EntReqMap[] = { +// request type entity type pts xtra? norml dist +{ Request::Type::WORKPLANE, Entity::Type::WORKPLANE, 1, false, true, false }, +{ Request::Type::DATUM_POINT, (Entity::Type)0, 1, false, false, false }, +{ Request::Type::LINE_SEGMENT, Entity::Type::LINE_SEGMENT, 2, false, false, false }, +{ Request::Type::CUBIC, Entity::Type::CUBIC, 4, true, false, false }, +{ Request::Type::CUBIC_PERIODIC, Entity::Type::CUBIC_PERIODIC, 3, true, false, false }, +{ Request::Type::CIRCLE, Entity::Type::CIRCLE, 1, false, true, true }, +{ Request::Type::ARC_OF_CIRCLE, Entity::Type::ARC_OF_CIRCLE, 3, false, true, false }, +{ Request::Type::TTF_TEXT, Entity::Type::TTF_TEXT, 4, false, true, false }, +{ Request::Type::IMAGE, Entity::Type::IMAGE, 4, false, true, false }, +}; + +static void CopyEntityInfo(const EntReqMapping *te, int extraPoints, + Entity::Type *ent, Request::Type *req, + int *pts, bool *hasNormal, bool *hasDistance) +{ + int points = te->points; + if(te->useExtraPoints) points += extraPoints; + + if(ent) *ent = te->entType; + if(req) *req = te->reqType; + if(pts) *pts = points; + if(hasNormal) *hasNormal = te->hasNormal; + if(hasDistance) *hasDistance = te->hasDistance; +} + +void EntReqTable::GetRequestInfo(Request::Type req, int extraPoints, + Entity::Type *ent, int *pts, bool *hasNormal, bool *hasDistance) +{ + for(const EntReqMapping &te : EntReqMap) { + if(req == te.reqType) { + CopyEntityInfo(&te, extraPoints, ent, NULL, pts, hasNormal, hasDistance); + return; + } + } + ssassert(false, "No request info"); +} + +bool EntReqTable::GetEntityInfo(Entity::Type ent, int extraPoints, + Request::Type *req, int *pts, bool *hasNormal, bool *hasDistance) +{ + for(const EntReqMapping &te : EntReqMap) { + if(ent == te.entType) { + CopyEntityInfo(&te, extraPoints, NULL, req, pts, hasNormal, hasDistance); + return true; + } + } + return false; +} + +Request::Type EntReqTable::GetRequestForEntity(Entity::Type ent) { + Request::Type req; + ssassert(GetEntityInfo(ent, 0, &req, NULL, NULL, NULL), + "No entity for request"); + return req; +} + +void Request::Generate(EntityList *entity, ParamList *param) +{ + int points = 0; + Entity::Type et = (Entity::Type)0; + bool hasNormal = false; + bool hasDistance = false; + int i; + + // Request-specific generation. + switch(type) { + case Type::TTF_TEXT: { + // `extraPoints` is storing kerning boolean + double actualAspectRatio = SS.fonts.AspectRatio(font, str, extraPoints); + if(EXACT(actualAspectRatio != 0.0)) { + // We could load the font, so use the actual value. + aspectRatio = actualAspectRatio; + } + if(EXACT(aspectRatio == 0.0)) { + // We couldn't load the font and we don't have anything saved, + // so just use 1:1, which is valid for the missing font symbol anyhow. + aspectRatio = 1.0; + } + break; + } + + case Type::IMAGE: { + auto image = SS.images.find(file); + if(image != SS.images.end()) { + std::shared_ptr pixmap = (*image).second; + if(pixmap != NULL) { + aspectRatio = (double)pixmap->width / (double)pixmap->height; + } + } + if(EXACT(aspectRatio == 0.0)) { + aspectRatio = 1.0; + } + break; + } + + default: // most requests don't do anything else + break; + } + + Entity e = {}; + EntReqTable::GetRequestInfo(type, extraPoints, &et, &points, &hasNormal, &hasDistance); + + // Generate the entity that's specific to this request. + e.type = et; + e.extraPoints = extraPoints; + e.group = group; + e.style = style; + e.workplane = workplane; + e.construction = construction; + e.str = str; + e.font = font; + e.file = file; + e.aspectRatio = aspectRatio; + e.h = h.entity(0); + + // And generate entities for the points + for(i = 0; i < points; i++) { + Entity p = {}; + p.workplane = workplane; + // points start from entity 1, except for datum point case + p.h = h.entity(i+((et != (Entity::Type)0) ? 1 : 0)); + p.group = group; + p.style = style; + p.construction = e.construction; + if(workplane == Entity::FREE_IN_3D) { + p.type = Entity::Type::POINT_IN_3D; + // params for x y z + p.param[0] = AddParam(param, h.param(16 + 3*i + 0)); + p.param[1] = AddParam(param, h.param(16 + 3*i + 1)); + p.param[2] = AddParam(param, h.param(16 + 3*i + 2)); + } else { + p.type = Entity::Type::POINT_IN_2D; + // params for u v + p.param[0] = AddParam(param, h.param(16 + 3*i + 0)); + p.param[1] = AddParam(param, h.param(16 + 3*i + 1)); + } + entity->Add(&p); + e.point[i] = p.h; + } + if(hasNormal) { + Entity n = {}; + n.workplane = workplane; + n.h = h.entity(32); + n.group = group; + n.style = style; + n.construction = e.construction; + if(workplane == Entity::FREE_IN_3D) { + n.type = Entity::Type::NORMAL_IN_3D; + n.param[0] = AddParam(param, h.param(32+0)); + n.param[1] = AddParam(param, h.param(32+1)); + n.param[2] = AddParam(param, h.param(32+2)); + n.param[3] = AddParam(param, h.param(32+3)); + } else { + n.type = Entity::Type::NORMAL_IN_2D; + // and this is just a copy of the workplane quaternion, + // so no params required + } + ssassert(points >= 1, "Positioning a normal requires a point"); + // The point determines where the normal gets displayed on-screen; + // it's entirely cosmetic. + n.point[0] = e.point[0]; + entity->Add(&n); + e.normal = n.h; + } + if(hasDistance) { + Entity d = {}; + d.workplane = workplane; + d.h = h.entity(64); + d.group = group; + d.style = style; + d.type = Entity::Type::DISTANCE; + d.param[0] = AddParam(param, h.param(64)); + entity->Add(&d); + e.distance = d.h; + } + + if(et != (Entity::Type)0) entity->Add(&e); +} + +std::string Request::DescriptionString() const { + const char *s = ""; + if(h == Request::HREQUEST_REFERENCE_XY) { + s = "#XY"; + } else if(h == Request::HREQUEST_REFERENCE_YZ) { + s = "#YZ"; + } else if(h == Request::HREQUEST_REFERENCE_ZX) { + s = "#ZX"; + } else { + switch(type) { + case Type::WORKPLANE: s = "workplane"; break; + case Type::DATUM_POINT: s = "datum-point"; break; + case Type::LINE_SEGMENT: s = "line-segment"; break; + case Type::CUBIC: s = "cubic-bezier"; break; + case Type::CUBIC_PERIODIC: s = "periodic-cubic"; break; + case Type::CIRCLE: s = "circle"; break; + case Type::ARC_OF_CIRCLE: s = "arc-of-circle"; break; + case Type::TTF_TEXT: s = "ttf-text"; break; + case Type::IMAGE: s = "image"; break; + } + } + ssassert(s != NULL, "Unexpected request type"); + return ssprintf("r%03x-%s", h.v, s); +} + +int Request::IndexOfPoint(hEntity he) const { + if(type == Type::DATUM_POINT) { + return (he == h.entity(0)) ? 0 : -1; + } + for(int i = 0; i < MAX_POINTS_IN_ENTITY; i++) { + if(he == h.entity(i + 1)) { + return i; + } + } + return -1; +} + +hParam Request::AddParam(ParamList *param, hParam hp) { + Param pa = {}; + pa.h = hp; + param->Add(&pa); + return hp; +} + +} // namespace SolveSpace diff --git a/src/resource.cpp b/src/resource.cpp new file mode 100644 index 0000000..8921d7a --- /dev/null +++ b/src/resource.cpp @@ -0,0 +1,1587 @@ +//----------------------------------------------------------------------------- +// Discovery and loading of our resources (icons, fonts, templates, etc). +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- +#include +#include +#include +#include "solvespace.h" + +namespace SolveSpace { + +//----------------------------------------------------------------------------- +// Resource loading functions +//----------------------------------------------------------------------------- + +std::string LoadString(const std::string &name) { + size_t size; + const void *data = Platform::LoadResource(name, &size); + std::string result(static_cast(data), size); + + // When editing resources under Windows, Windows newlines may sneak in. + // Any files with them won't be merged, but ignoring them during development + // helps external contributors. + result.erase(std::remove(result.begin(), result.end(), '\r'), + result.end()); + + return result; +} + +std::string LoadStringFromGzip(const std::string &name) { + size_t deflatedSize; + const void *data = Platform::LoadResource(name, &deflatedSize); + + z_stream stream; + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + const int windowBits = /*maximum window*/ 15 + /*decode gzip header*/16; + ssassert(inflateInit2(&stream, windowBits) == Z_OK, "Cannot start inflation"); + + // Extract length mod 2**32 from the gzip trailer. + std::string result; + ssassert(deflatedSize >= 4, "Resource too small to have gzip trailer"); + + // *(uint32_t *) may perform an unaligned access, so do a memcpy. + uint32_t inflatedSize; + memcpy(&inflatedSize, (uint8_t *)((uintptr_t)data + deflatedSize - 4), sizeof(uint32_t)); + result.resize(inflatedSize); + + stream.next_in = (Bytef *)data; + stream.avail_in = (uInt)deflatedSize; + stream.next_out = (Bytef *)&result[0]; + stream.avail_out = (uInt)result.length(); + ssassert(inflate(&stream, Z_NO_FLUSH) == Z_STREAM_END, "Cannot inflate resource"); + ssassert(stream.avail_out == 0, "Inflated resource larger than what trailer indicates"); + + inflateEnd(&stream); + + return result; +} + +std::shared_ptr LoadPng(const std::string &name) { + size_t size; + const void *data = Platform::LoadResource(name, &size); + + std::shared_ptr pixmap = Pixmap::FromPng(static_cast(data), size); + ssassert(pixmap != nullptr, "Cannot load pixmap"); + + return pixmap; +} + +//----------------------------------------------------------------------------- +// Pixmap manipulation +//----------------------------------------------------------------------------- + +size_t Pixmap::GetBytesPerPixel() const { + switch(format) { + case Format::RGBA: return 4; + case Format::BGRA: return 4; + case Format::RGB: return 3; + case Format::BGR: return 3; + case Format::A: return 1; + } + ssassert(false, "Unexpected pixmap format"); +} + +RgbaColor Pixmap::GetPixel(size_t x, size_t y) const { + const uint8_t *pixel = &data[y * stride + x * GetBytesPerPixel()]; + + switch(format) { + case Format::RGBA: + return RgbaColor::From(pixel[0], pixel[1], pixel[2], pixel[3]); + + case Format::RGB: + return RgbaColor::From(pixel[0], pixel[1], pixel[2], 255); + + case Format::BGRA: + return RgbaColor::From(pixel[2], pixel[1], pixel[0], pixel[3]); + + case Format::BGR: + return RgbaColor::From(pixel[2], pixel[1], pixel[0], 255); + + case Format::A: + return RgbaColor::From( 255, 255, 255, pixel[0]); + } + ssassert(false, "Unexpected resource format"); +} + +void Pixmap::SetPixel(size_t x, size_t y, RgbaColor color) { + uint8_t *pixel = &data[y * stride + x * GetBytesPerPixel()]; + + switch(format) { + case Format::RGBA: + pixel[0] = color.red; + pixel[1] = color.green; + pixel[2] = color.blue; + pixel[3] = color.alpha; + break; + + case Format::RGB: + pixel[0] = color.red; + pixel[1] = color.green; + pixel[2] = color.blue; + break; + + case Format::BGRA: + pixel[0] = color.blue; + pixel[1] = color.green; + pixel[2] = color.red; + pixel[3] = color.alpha; + break; + + case Format::BGR: + pixel[0] = color.blue; + pixel[1] = color.green; + pixel[2] = color.red; + break; + + case Format::A: + pixel[0] = color.alpha; + break; + } +} + +void Pixmap::ConvertTo(Format newFormat) { + switch(format) { + case Format::RGBA: + ssassert(newFormat == Format::BGRA, "Unexpected target format"); + break; + + case Format::BGRA: + ssassert(newFormat == Format::RGBA, "Unexpected target format"); + break; + + case Format::RGB: + ssassert(newFormat == Format::BGR, "Unexpected target format"); + break; + + case Format::BGR: + ssassert(newFormat == Format::RGB, "Unexpected target format"); + break; + + case Format::A: + ssassert(false, "Unexpected target format"); + } + + size_t bpp = GetBytesPerPixel(); + for(size_t j = 0; j != height; j++) { + uint8_t *row = &data[j * stride]; + for(size_t i = 0; i != width * bpp; i += bpp) { + // This handles both RGB<>BGR and RGBA<>BGRA. + std::swap(row[i], row[i + 2]); + } + } + + format = newFormat; +} + +static std::shared_ptr ReadPngIntoPixmap(png_struct *png_ptr, png_info *info_ptr, + bool flip) { + png_read_png(png_ptr, info_ptr, + PNG_TRANSFORM_EXPAND | PNG_TRANSFORM_GRAY_TO_RGB | PNG_TRANSFORM_SCALE_16, NULL); + + std::shared_ptr pixmap = std::make_shared(); + pixmap->width = png_get_image_width(png_ptr, info_ptr); + pixmap->height = png_get_image_height(png_ptr, info_ptr); + if((png_get_color_type(png_ptr, info_ptr) & PNG_COLOR_MASK_ALPHA) != 0) { + pixmap->format = Pixmap::Format::RGBA; + } else { + pixmap->format = Pixmap::Format::RGB; + } + + size_t stride = pixmap->width * pixmap->GetBytesPerPixel(); + if(stride % 4 != 0) stride += 4 - stride % 4; + pixmap->stride = stride; + + pixmap->data = std::vector(pixmap->stride * pixmap->height); + uint8_t **rows = png_get_rows(png_ptr, info_ptr); + for(size_t y = 0; y < pixmap->height; y++) { + uint8_t *srcRow = flip ? rows[pixmap->height - y - 1] : rows[y]; + memcpy(&pixmap->data[pixmap->stride * y], srcRow, + pixmap->width * pixmap->GetBytesPerPixel()); + } + + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); + return pixmap; +} + +std::shared_ptr Pixmap::FromPng(const uint8_t *data, size_t size, bool flip) { + struct Slice { const uint8_t *data; size_t size; }; + Slice dataSlice = { data, size }; + png_struct *png_ptr = NULL; + png_info *info_ptr = NULL; + + png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if(!png_ptr) goto exit; + info_ptr = png_create_info_struct(png_ptr); + if(!info_ptr) goto exit; + + if(setjmp(png_jmpbuf(png_ptr))) goto exit; + + png_set_read_fn(png_ptr, &dataSlice, + [](png_struct *png_ptr, uint8_t *data, size_t size) { + Slice *dataSlice = (Slice *)png_get_io_ptr(png_ptr); + if(size <= dataSlice->size) { + memcpy(data, dataSlice->data, size); + dataSlice->data += size; + dataSlice->size -= size; + } else { + png_error(png_ptr, "EOF"); + } + }); + + return ReadPngIntoPixmap(png_ptr, info_ptr, flip); + +exit: + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); + return nullptr; +} + +std::shared_ptr Pixmap::ReadPng(FILE *f, bool flip) { + png_struct *png_ptr = NULL; + png_info *info_ptr = NULL; + + uint8_t header[8]; + if(fread(header, 1, sizeof(header), f) != sizeof(header)) goto exit; + if(png_sig_cmp(header, 0, sizeof(header))) goto exit; + + png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if(!png_ptr) goto exit; + info_ptr = png_create_info_struct(png_ptr); + if(!info_ptr) goto exit; + + if(setjmp(png_jmpbuf(png_ptr))) goto exit; + + png_init_io(png_ptr, f); + png_set_sig_bytes(png_ptr, sizeof(header)); + + return ReadPngIntoPixmap(png_ptr, info_ptr, flip); + +exit: + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); + return nullptr; +} + +std::shared_ptr Pixmap::ReadPng(const Platform::Path &filename, bool flip) { + FILE *f = OpenFile(filename, "rb"); + if(!f) return NULL; + std::shared_ptr pixmap = ReadPng(f, flip); + fclose(f); + return pixmap; +} + +bool Pixmap::WritePng(FILE *f, bool flip) { + colorType = 0; + bgr = false; + switch(format) { + case Format::RGBA: colorType = PNG_COLOR_TYPE_RGBA; bgr = false; break; + case Format::BGRA: colorType = PNG_COLOR_TYPE_RGBA; bgr = true; break; + case Format::RGB: colorType = PNG_COLOR_TYPE_RGB; bgr = false; break; + case Format::BGR: colorType = PNG_COLOR_TYPE_RGB; bgr = true; break; + case Format::A: colorType = PNG_COLOR_TYPE_GRAY; bgr = false; break; + } + + std::vector rows; + for(size_t y = 0; y < height; y++) { + if(flip) { + rows.push_back(&data[stride * (height - y - 1)]); + } else { + rows.push_back(&data[stride * y]); + } + } + + png_struct *png_ptr = NULL; + png_info *info_ptr = NULL; + + png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if(!png_ptr) goto exit; + info_ptr = png_create_info_struct(png_ptr); + if(!info_ptr) goto exit; + + if(setjmp(png_jmpbuf(png_ptr))) goto exit; + + png_init_io(png_ptr, f); + png_set_IHDR(png_ptr, info_ptr, + (png_uint_32)width, (png_uint_32)height, 8, colorType, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + if(bgr) png_set_bgr(png_ptr); + png_write_info(png_ptr, info_ptr); + png_write_image(png_ptr, &rows[0]); + png_write_end(png_ptr, info_ptr); + + png_destroy_write_struct(&png_ptr, &info_ptr); + return true; + +exit: + png_destroy_write_struct(&png_ptr, &info_ptr); + return false; +} + +bool Pixmap::WritePng(const Platform::Path &filename, bool flip) { + FILE *f = OpenFile(filename, "wb"); + if(!f) return false; + bool success = WritePng(f, flip); + fclose(f); + return success; +} + +bool Pixmap::Equals(const Pixmap &other) const { + if(format != other.format || width != other.width || height != other.height) { + return false; + } + + size_t rowLength = width * GetBytesPerPixel(); + for(size_t y = 0; y < height; y++) { + if(memcmp(&data[y * stride], &other.data[y * other.stride], rowLength)) { + return false; + } + } + + return true; +} + +std::shared_ptr Pixmap::Create(Format format, size_t width, size_t height) { + std::shared_ptr pixmap = std::make_shared(); + pixmap->format = format; + pixmap->width = width; + pixmap->height = height; + // Align to fulfill OpenGL texture requirements. + size_t stride = pixmap->width * pixmap->GetBytesPerPixel(); + if(stride % 4 != 0) stride += 4 - stride % 4; + pixmap->stride = stride; + pixmap->data = std::vector(pixmap->stride * pixmap->height); + return pixmap; +} + +std::shared_ptr Pixmap::Copy() { + std::shared_ptr pixmap = std::make_shared(); + pixmap->format = format; + pixmap->width = width; + pixmap->height = height; + pixmap->stride = stride; + pixmap->data = data; + return pixmap; +} + +//----------------------------------------------------------------------------- +// ASCII sequence parsing +//----------------------------------------------------------------------------- + +class ASCIIReader { +public: + std::string::const_iterator pos, end; + + static ASCIIReader From(const std::string &str) { + return ASCIIReader({ str.cbegin(), str.cend() }); + } + + bool AtEnd() const { + return pos == end; + } + + bool SkipSpace() { + bool skipped = false; + while(!AtEnd()) { + char c = *pos; + if(!(c == ' ' || c == '\t' || c == '\n')) break; + skipped = true; + pos++; + } + return skipped; + } + + char PeekChar() { + ssassert(!AtEnd(), "Unexpected EOF"); + return *pos; + } + + char ReadChar() { + ssassert(!AtEnd(), "Unexpected EOF"); + return *pos++; + } + + bool TryChar(char c) { + if(AtEnd()) { + return false; + } else if(*pos == c) { + pos++; + return true; + } else { + return false; + } + } + + void ExpectChar(char c) { + if(!TryChar(c)) { + dbp("Expecting character '%c'", c); + ssassert(false, "Unexpected character"); + } + } + + bool TryString(const std::string &s) { + if((size_t)(end - pos) >= s.size() && std::string(pos, pos + s.size()) == s) { + pos += s.size(); + return true; + } else { + return false; + } + } + + void ExpectString(const std::string &s) { + if(!TryString(s)) { + dbp("Expecting string '%s'", s.c_str()); + ssassert(false, "Unexpected string"); + } + } + + size_t CountUntilEol() const { + return std::find(pos, end, '\n') - pos; + } + + void SkipUntilEol() { + pos = std::find(pos, end, '\n'); + } + + std::string ReadUntilEol() { + auto eol = std::find(pos, end, '\n'); + std::string result(pos, eol); + if(eol != end) { + pos = eol + 1; + } else { + pos = end; + } + return result; + } + + uint8_t Read4HexBits() { + char c = ReadChar(); + if(c >= '0' && c <= '9') { + return c - '0'; + } else if(c >= 'a' && c <= 'f') { + return 10 + (c - 'a'); + } else if(c >= 'A' && c <= 'F') { + return 10 + (c - 'A'); + } else ssassert(false, "Unexpected hex digit"); + } + + uint8_t Read8HexBits() { + uint8_t h = Read4HexBits(), + l = Read4HexBits(); + return (h << 4) + l; + } + + uint16_t Read16HexBits() { + uint16_t h = Read8HexBits(), + l = Read8HexBits(); + return (h << 8) + l; + } + + long ReadIntegerDecimal(int base = 10) { + char *endptr; + long l = strtol(&*pos, &endptr, base); + ssassert(&*pos != endptr, "Cannot read an integer number"); + pos += endptr - &*pos; + return l; + } + + double ReadFloatDecimal() { + char *endptr; + double d = strtod(&*pos, &endptr); + ssassert(&*pos != endptr, "Cannot read a floating-point number"); + pos += endptr - &*pos; + return d; + } + + bool TryRegex(const std::regex &re, std::smatch *m) { + if(std::regex_search(pos, end, *m, re, std::regex_constants::match_continuous)) { + pos += m->length(); + return true; + } else { + return false; + } + } + + void ExpectRegex(const std::regex &re, std::smatch *m) { + ssassert(TryRegex(re, m), "Unmatched regex"); + } +}; + +//----------------------------------------------------------------------------- +// Bitmap font manipulation +//----------------------------------------------------------------------------- + +static uint8_t *BitmapFontTextureRow(std::shared_ptr texture, + uint16_t position, size_t y) { + // position = 0; + size_t col = position % (texture->width / 16), + row = position / (texture->width / 16); + return &texture->data[texture->stride * (16 * row + y) + 16 * col]; +} + +BitmapFont BitmapFont::From(std::string &&unifontData) { + BitmapFont font = {}; + font.unifontData = std::move(unifontData); + font.texture = Pixmap::Create(Pixmap::Format::A, 1024, 1024); + + return font; +} + +void BitmapFont::AddGlyph(char32_t codepoint, std::shared_ptr pixmap) { + ssassert((pixmap->width == 8 || pixmap->width == 16) && pixmap->height == 16, + "Unexpected pixmap dimensions"); + ssassert(pixmap->format == Pixmap::Format::RGB, + "Unexpected pixmap format"); + ssassert(glyphs.find(codepoint) == glyphs.end(), + "Glyph with this codepoint already exists"); + ssassert(nextPosition != 0xffff, + "Too many glyphs for current texture size"); + + BitmapFont::Glyph glyph = {}; + glyph.advanceCells = (uint8_t)(pixmap->width / 8); + glyph.position = nextPosition++; + glyphs.emplace(codepoint, glyph); + + for(size_t y = 0; y < pixmap->height; y++) { + uint8_t *row = BitmapFontTextureRow(texture, glyph.position, y); + for(size_t x = 0; x < pixmap->width; x++) { + if((pixmap->GetPixel(x, y).ToPackedInt() & 0xffffff) != 0) { + row[x] = 255; + } + } + } +} + +const BitmapFont::Glyph &BitmapFont::GetGlyph(char32_t codepoint) { + auto it = glyphs.find(codepoint); + if(it != glyphs.end()) { + return (*it).second; + } + + ssassert(nextPosition != 0xffff, + "Too many glyphs for current texture size"); + + // Find the hex representation in the (sorted) Unifont file. + auto first = unifontData.cbegin(), + last = unifontData.cend(); + while(first < last) { + auto mid = first + (last - first) / 2; + while(mid != unifontData.cbegin()) { + if(*mid == '\n') { + mid++; + break; + } + mid--; + } + + ASCIIReader reader = { mid, unifontData.cend() }; + if(reader.AtEnd()) break; + + // Read the codepoint. + char32_t foundCodepoint = reader.Read16HexBits(); + reader.ExpectChar(':'); + + if(foundCodepoint > codepoint) { + last = mid - 1; + continue; // and first stays the same + } + if(foundCodepoint < codepoint) { + first = mid + 1; + while(first != unifontData.cend()) { + if(*first == '\n') { + first++; + break; + } + first++; + } + continue; // and last stays the same + } + + // Found the codepoint. + Glyph glyph = {}; + glyph.position = nextPosition++; + + // Read glyph bits. + unsigned short glyphBits[16]; + size_t glyphLength = reader.CountUntilEol(); + if(glyphLength == 4 * 16) { + glyph.advanceCells = 2; + for(size_t i = 0; i < 16; i++) { + glyphBits[i] = reader.Read16HexBits(); + } + } else if(glyphLength == 2 * 16) { + glyph.advanceCells = 1; + for(size_t i = 0; i < 16; i++) { + glyphBits[i] = (uint16_t)reader.Read8HexBits() << 8; + } + } else ssassert(false, "Unexpected glyph bitmap length"); + + // Fill in the texture (one texture byte per glyph bit). + for(size_t y = 0; y < 16; y++) { + uint8_t *row = BitmapFontTextureRow(texture, glyph.position, y); + for(size_t x = 0; x < 16; x++) { + if(glyphBits[y] & (1 << (15 - x))) { + row[x] = 255; + } + } + } + + it = glyphs.emplace(codepoint, glyph).first; + + textureUpdated = true; + return (*it).second; + } + + // Glyph doesn't exist; return replacement glyph instead. + ssassert(codepoint != 0xfffd, "Cannot parse replacement glyph"); + return GetGlyph(0xfffd); +} + +void BitmapFont::LocateGlyph(char32_t codepoint, + double *s0, double *t0, double *s1, double *t1, + size_t *w, size_t *h) { + const Glyph &glyph = GetGlyph(codepoint); + *w = glyph.advanceCells * 8; + *h = 16; + *s0 = (16.0 * (glyph.position % (texture->width / 16))) / texture->width; + *s1 = *s0 + (double)(*w) / texture->width; + *t0 = (16.0 * (glyph.position / (texture->width / 16))) / texture->height; + *t1 = *t0 + (double)(*h) / texture->height; +} + +size_t BitmapFont::GetWidth(char32_t codepoint) { + if(codepoint >= 0xe000 && codepoint <= 0xefff) { + // These are special-cased because checkboxes predate support for 2 cell wide + // characters; and so all Printf() calls pad them with spaces. + return 1; + } + + return GetGlyph(codepoint).advanceCells; +} + +size_t BitmapFont::GetWidth(const std::string &str) { + size_t width = 0; + for(char32_t codepoint : ReadUTF8(str)) { + width += GetWidth(codepoint); + } + return width; +} + +BitmapFont BitmapFont::Create() { + BitmapFont Font = BitmapFont::From(LoadStringFromGzip("fonts/unifont.hex.gz")); + // Unifont doesn't have a glyph for U+0020. + Font.AddGlyph(0x0020, Pixmap::Create(Pixmap::Format::RGB, 8, 16)); + Font.AddGlyph(0xE000, LoadPng("fonts/private/0-check-false.png")); + Font.AddGlyph(0xE001, LoadPng("fonts/private/1-check-true.png")); + Font.AddGlyph(0xE002, LoadPng("fonts/private/2-radio-false.png")); + Font.AddGlyph(0xE003, LoadPng("fonts/private/3-radio-true.png")); + Font.AddGlyph(0xE004, LoadPng("fonts/private/4-stipple-dot.png")); + Font.AddGlyph(0xE005, LoadPng("fonts/private/5-stipple-dash-long.png")); + Font.AddGlyph(0xE006, LoadPng("fonts/private/6-stipple-dash.png")); + Font.AddGlyph(0xE007, LoadPng("fonts/private/7-stipple-zigzag.png")); + return Font; +} + +//----------------------------------------------------------------------------- +// Vector font manipulation +//----------------------------------------------------------------------------- + +const static int ARC_POINTS = 8; +static void MakePwlArc(VectorFont::Contour *contour, bool isReversed, + const Point2d &cp, double radius, double a1, double a2) { + if(radius < LENGTH_EPS) return; + + double aSign = 1.0; + if(isReversed) { + if(a1 <= a2 + LENGTH_EPS) a1 += 2.0 * M_PI; + aSign = -1.0; + } else { + if(a2 <= a1 + LENGTH_EPS) a2 += 2.0 * M_PI; + } + + double aStep = aSign * fabs(a2 - a1) / (double)ARC_POINTS; + for(int i = 0; i <= ARC_POINTS; i++) { + contour->points.emplace_back(cp.Plus(Point2d::FromPolar(radius, a1 + aStep * i))); + } +} + +static void MakePwlBulge(VectorFont::Contour *contour, const Point2d &v, double bulge) { + bool reversed = bulge < 0.0; + double alpha = atan(bulge) * 4.0; + const Point2d &point = contour->points.back(); + + Point2d middle = point.Plus(v).ScaledBy(0.5); + double dist = point.DistanceTo(v) / 2.0; + double angle = point.AngleTo(v); + + // alpha can't be 0.0 at this point + double radius = fabs(dist / sin(alpha / 2.0)); + double wu = fabs(radius*radius - dist*dist); + double h = sqrt(wu); + + if(bulge > 0.0) { + angle += M_PI_2; + } else { + angle -= M_PI_2; + } + + if(fabs(alpha) > M_PI) { + h = -h; + } + + Point2d center = Point2d::FromPolar(h, angle).Plus(middle); + double a1 = center.AngleTo(point); + double a2 = center.AngleTo(v); + MakePwlArc(contour, reversed, center, radius, a1, a2); +} + +static void GetGlyphBBox(const VectorFont::Glyph &glyph, + double *rminx, double *rmaxx, double *rminy, double *rmaxy) { + double minx = 0.0, maxx = 0.0, miny = 0.0, maxy = 0.0; + if(!glyph.contours.empty()) { + const Point2d &start = glyph.contours[0].points[0]; + minx = maxx = start.x; + miny = maxy = start.y; + for(const VectorFont::Contour &c : glyph.contours) { + for(const Point2d &p : c.points) { + maxx = std::max(maxx, p.x); + minx = std::min(minx, p.x); + maxy = std::max(maxy, p.y); + miny = std::min(miny, p.y); + } + } + } + + if(rminx) *rminx = minx; + if(rmaxx) *rmaxx = maxx; + if(rminy) *rminy = miny; + if(rmaxy) *rmaxy = maxy; +} + +VectorFont VectorFont::From(std::string &&lffData) { + VectorFont font = {}; + font.lffData = std::move(lffData); + + ASCIIReader reader = ASCIIReader::From(font.lffData); + std::smatch m; + while(reader.TryRegex(std::regex("#\\s*(\\w+)\\s*:\\s*(.+?)\n"), &m)) { + std::string name = m.str(1), + value = m.str(2); + std::transform(name.begin(), name.end(), name.begin(), ::tolower); + if(name == "letterspacing") { + font.rightSideBearing = std::stod(value); + } else if(name == "wordspacing") { + Glyph space = {}; + space.advanceWidth = std::stod(value); + font.glyphs.emplace(' ', std::move(space)); + } + } + + GetGlyphBBox(font.GetGlyph('A'), nullptr, nullptr, nullptr, &font.capHeight); + GetGlyphBBox(font.GetGlyph('h'), nullptr, nullptr, nullptr, &font.ascender); + GetGlyphBBox(font.GetGlyph('p'), nullptr, nullptr, &font.descender, nullptr); + + ssassert(!font.IsEmpty(), "Expected to load a font"); + return font; +} + +const VectorFont::Glyph &VectorFont::GetGlyph(char32_t codepoint) { + auto it = glyphs.find(codepoint); + if(it != glyphs.end()) { + return (*it).second; + } + + auto firstGlyph = std::find(lffData.cbegin(), lffData.cend(), '['); + ssassert(firstGlyph != lffData.cend(), "Vector font contains no glyphs"); + + // Find the serialized representation in the (sorted) lff file. + auto first = firstGlyph, + last = lffData.cend(); + while(first <= last) { + auto mid = first + (last - first) / 2; + while(mid > first) { + if(*mid == '[' && *(mid - 1) == '\n') break; + mid--; + } + + ASCIIReader reader = { mid, lffData.cend() }; + if(reader.AtEnd()) break; + + // Read the codepoint. + reader.ExpectChar('['); + char32_t foundCodepoint = reader.Read16HexBits(); + reader.ExpectChar(']'); + reader.SkipUntilEol(); + + if(foundCodepoint > codepoint) { + last = mid - 1; + continue; // and first stays the same + } + if(foundCodepoint < codepoint) { + first = mid + 1; + while(first != lffData.cend()) { + if(*first == '[' && *(first - 1) == '\n') break; + first++; + } + continue; // and last stays the same + } + + // Found the codepoint. + VectorFont::Glyph glyph = {}; + + // Read glyph contours. + while(!reader.AtEnd()) { + if(reader.TryChar('\n')) { + // Skip. + } else if(reader.TryChar('[')) { + // End of glyph. + if(glyph.contours.back().points.empty()) { + // Remove an useless empty contour, if any. + glyph.contours.pop_back(); + } + break; + } else if(reader.TryChar('C')) { + // Another character is referenced in this one. + char32_t baseCodepoint = reader.Read16HexBits(); + const VectorFont::Glyph &baseGlyph = GetGlyph(baseCodepoint); + std::copy(baseGlyph.contours.begin(), baseGlyph.contours.end(), + std::back_inserter(glyph.contours)); + } else { + Contour contour; + do { + Point2d p; + p.x = reader.ReadFloatDecimal(); + reader.ExpectChar(','); + p.y = reader.ReadFloatDecimal(); + + if(reader.TryChar(',')) { + // Point with a bulge. + reader.ExpectChar('A'); + double bulge = reader.ReadFloatDecimal(); + MakePwlBulge(&contour, p, bulge); + } else { + // Just a point. + contour.points.emplace_back(std::move(p)); + } + } while(reader.TryChar(';')); + reader.ExpectChar('\n'); + glyph.contours.emplace_back(std::move(contour)); + } + } + + // Calculate metrics. + GetGlyphBBox(glyph, &glyph.leftSideBearing, &glyph.boundingWidth, nullptr, nullptr); + glyph.advanceWidth = glyph.leftSideBearing + glyph.boundingWidth + rightSideBearing; + + it = glyphs.emplace(codepoint, std::move(glyph)).first; + return (*it).second; + } + + // Glyph doesn't exist; return replacement glyph instead. + ssassert(codepoint != 0xfffd, "Cannot parse replacement glyph"); + return GetGlyph(0xfffd); +} + +VectorFont *VectorFont::Builtin() { + static VectorFont Font; + if(Font.IsEmpty()) { + Font = VectorFont::From(LoadStringFromGzip("fonts/unicode.lff.gz")); + } + return &Font; +} + +double VectorFont::GetCapHeight(double forCapHeight) const { + ssassert(!IsEmpty(), "Expected a loaded font"); + + return forCapHeight; +} + +double VectorFont::GetHeight(double forCapHeight) const { + ssassert(!IsEmpty(), "Expected a loaded font"); + + return (ascender - descender) * (forCapHeight / capHeight); +} + +double VectorFont::GetWidth(double forCapHeight, const std::string &str) { + ssassert(!IsEmpty(), "Expected a loaded font"); + + double width = 0; + for(char32_t codepoint : ReadUTF8(str)) { + width += GetGlyph(codepoint).advanceWidth; + } + width -= rightSideBearing; + return width * (forCapHeight / capHeight); +} + +Vector VectorFont::GetExtents(double forCapHeight, const std::string &str) { + Vector ex = {}; + ex.x = GetWidth(forCapHeight, str); + ex.y = GetHeight(forCapHeight); + return ex; +} + +void VectorFont::Trace(double forCapHeight, Vector o, Vector u, Vector v, const std::string &str, + const std::function &traceEdge) { + ssassert(!IsEmpty(), "Expected a loaded font"); + + double scale = (forCapHeight / capHeight); + u = u.ScaledBy(scale); + v = v.ScaledBy(scale); + + for(char32_t codepoint : ReadUTF8(str)) { + const Glyph &glyph = GetGlyph(codepoint); + + for(const VectorFont::Contour &contour : glyph.contours) { + Vector prevp; + bool penUp = true; + for(const Point2d &pt : contour.points) { + Vector p = o.Plus(u.ScaledBy(pt.x)) + .Plus(v.ScaledBy(pt.y)); + if(!penUp) traceEdge(prevp, p); + prevp = p; + penUp = false; + } + } + + o = o.Plus(u.ScaledBy(glyph.advanceWidth)); + } +} + +void VectorFont::Trace(double forCapHeight, Vector o, Vector u, Vector v, const std::string &str, + const std::function &traceEdge, const Camera &camera) { + ssassert(!IsEmpty(), "Expected a loaded font"); + + // Perform grid-fitting only when the text is parallel to the view plane. + if(camera.gridFit && !(u.WithMagnitude(1).Equals(camera.projRight) && + v.WithMagnitude(1).Equals(camera.projUp))) { + return Trace(forCapHeight, o, u, v, str, traceEdge); + } + + double scale = forCapHeight / capHeight; + u = u.ScaledBy(scale); + v = v.ScaledBy(scale); + + for(char32_t codepoint : ReadUTF8(str)) { + const Glyph &glyph = GetGlyph(codepoint); + double actualWidth = std::max(1.0, glyph.boundingWidth); + + // Align (o+lsb), (o+lsb+u) and (o+lsb+v) to pixel grid. + Vector ao = o.Plus(u.ScaledBy(glyph.leftSideBearing)); + Vector au = ao.Plus(u.ScaledBy(actualWidth)); + Vector av = ao.Plus(v.ScaledBy(capHeight)); + + ao = camera.AlignToPixelGrid(ao); + au = camera.AlignToPixelGrid(au); + av = camera.AlignToPixelGrid(av); + + au = au.Minus(ao).ScaledBy(1.0 / actualWidth); + av = av.Minus(ao).ScaledBy(1.0 / capHeight); + + for(const VectorFont::Contour &contour : glyph.contours) { + Vector prevp; + bool penUp = true; + for(const Point2d &pt : contour.points) { + Vector p = ao.Plus(au.ScaledBy(pt.x - glyph.leftSideBearing)) + .Plus(av.ScaledBy(pt.y)); + if(!penUp) traceEdge(prevp, p); + prevp = p; + penUp = false; + } + } + + o = o.Plus(u.ScaledBy(glyph.advanceWidth)); + } +} + +//----------------------------------------------------------------------------- +// Gettext plural expression evaluation +//----------------------------------------------------------------------------- + +class PluralExpr { +public: + class Token { + public: + enum class Type { + END, + VALUE, + BINARY_OP, + QUERY, + COLON, + PAREN_LEFT, + PAREN_RIGHT, + }; + + // Only valid for type == BINARY_OP. + enum class Op { + NONE, + // comparison + EQ, // == + NEQ, // != + LT, // < + GT, // > + LE, // <= + GE, // >= + // logical + AND, // && + OR, // || + // arithmetic + MOD, // % + }; + + Type type; + Op op; + unsigned value; + + int Precedence(); + }; + + ASCIIReader reader; + std::vector stack; + unsigned value; + + Token Lex(); + + Token PopToken(); + void Reduce(); + void Eval(); + + static unsigned Eval(const std::string &s, unsigned n); +}; + +int PluralExpr::Token::Precedence() { + switch(type) { + case Type::BINARY_OP: + switch(op) { + case Op::MOD: + return 7; + + case Op::LT: + case Op::GT: + case Op::LE: + case Op::GE: + return 6; + + case Op::EQ: + case Op::NEQ: + return 5; + + case Op::AND: + return 4; + + case Op::OR: + return 3; + + case Op::NONE: + ; + } + ssassert(false, "Unexpected operator"); + + case Type::QUERY: + case Type::COLON: + return 1; + + case Type::VALUE: + return 0; + + default: + ssassert(false, "Unexpected token op"); + } +} + +PluralExpr::Token PluralExpr::Lex() { + Token t = {}; + + reader.SkipSpace(); + + char c = reader.PeekChar(); + if(c >= '0' && c <= '9') { + t.type = Token::Type::VALUE; + t.value = reader.ReadIntegerDecimal(); + } else if(reader.TryChar('n')) { + t.type = Token::Type::VALUE; + t.value = value; + } else if(reader.TryChar('%')) { + t.type = Token::Type::BINARY_OP; + t.op = Token::Op::MOD; + } else if(reader.TryChar('<')) { + t.type = Token::Type::BINARY_OP; + if(reader.TryChar('=')) { + t.op = Token::Op::LE; + } else { + t.op = Token::Op::LT; + } + } else if(reader.TryChar('>')) { + t.type = Token::Type::BINARY_OP; + if(reader.TryChar('=')) { + t.op = Token::Op::GE; + } else { + t.op = Token::Op::GT; + } + } else if(reader.TryChar('!')) { + reader.ExpectChar('='); + t.type = Token::Type::BINARY_OP; + t.op = Token::Op::NEQ; + } else if(reader.TryChar('=')) { + reader.ExpectChar('='); + t.type = Token::Type::BINARY_OP; + t.op = Token::Op::EQ; + } else if(reader.TryChar('&')) { + reader.ExpectChar('&'); + t.type = Token::Type::BINARY_OP; + t.op = Token::Op::AND; + } else if(reader.TryChar('|')) { + reader.ExpectChar('|'); + t.type = Token::Type::BINARY_OP; + t.op = Token::Op::OR; + } else if(reader.TryChar('?')) { + t.type = Token::Type::QUERY; + } else if(reader.TryChar(':')) { + t.type = Token::Type::COLON; + } else if(reader.TryChar('(')) { + t.type = Token::Type::PAREN_LEFT; + } else if(reader.TryChar(')')) { + t.type = Token::Type::PAREN_RIGHT; + } else if(reader.AtEnd()) { + t.type = Token::Type::END; + } else { + ssassert(false, "Unexpected character"); + } + + return t; +} + +PluralExpr::Token PluralExpr::PopToken() { + ssassert(!stack.empty(), "Expected a non-empty stack"); + Token t = stack.back(); + stack.pop_back(); + return t; +} + +void PluralExpr::Reduce() { + Token r; + r.type = Token::Type::VALUE; + + Token a = PopToken(); + ssassert(a.type == Token::Type::VALUE, "Expected 1st operand to be a value"); + + Token op = PopToken(); + switch(op.type) { + case Token::Type::BINARY_OP: { + Token b = PopToken(); + ssassert(b.type == Token::Type::VALUE, "Expected 2nd operand to be a value"); + + switch(op.op) { + case Token::Op::EQ: + r.value = (a.value == b.value ? 1 : 0); + break; + case Token::Op::NEQ: + r.value = (a.value != b.value ? 1 : 0); + break; + case Token::Op::LT: + r.value = (b.value < a.value ? 1 : 0); + break; + case Token::Op::GT: + r.value = (b.value > a.value ? 1 : 0); + break; + case Token::Op::LE: + r.value = (b.value <= a.value ? 1 : 0); + break; + case Token::Op::GE: + r.value = (b.value >= a.value ? 1 : 0); + break; + case Token::Op::AND: + r.value = a.value && b.value; + break; + case Token::Op::OR: + r.value = a.value || b.value; + break; + case Token::Op::MOD: + r.value = b.value % a.value; + break; + case Token::Op::NONE: + ssassert(false, "Unexpected operator"); + } + break; + } + + case Token::Type::COLON: { + Token b = PopToken(); + ssassert(PopToken().type == Token::Type::QUERY, "Expected ?"); + Token c = PopToken(); + r.value = c.value ? b.value : a.value; + break; + } + + default: + ssassert(false, "Unexpected operator type"); + } + + stack.push_back(r); +} + +void PluralExpr::Eval() { + while(true) { + Token t = Lex(); + switch(t.type) { + case Token::Type::END: + case Token::Type::PAREN_RIGHT: + while(stack.size() > 1 && + stack.end()[-2].type != Token::Type::PAREN_LEFT) { + Reduce(); + } + if(t.type == Token::Type::PAREN_RIGHT) { + ssassert(stack.size() > 1, "Expected ("); + stack.push_back(t); + } + return; + + case Token::Type::PAREN_LEFT: + stack.push_back(t); + Eval(); + if(stack.back().type != Token::Type::PAREN_RIGHT) { + ssassert(false, "Expected )"); + } + stack.pop_back(); + stack.erase(stack.end() - 2); + break; + + case Token::Type::VALUE: + stack.push_back(t); + break; + + case Token::Type::BINARY_OP: + case Token::Type::QUERY: + case Token::Type::COLON: + while(stack.size() > 1 && + stack.end()[-2].type != Token::Type::PAREN_LEFT && + t.Precedence() < stack.end()[-2].Precedence()) { + Reduce(); + } + stack.push_back(t); + break; + } + } +} + +unsigned PluralExpr::Eval(const std::string &s, unsigned n) { + PluralExpr expr = {}; + expr.reader = ASCIIReader::From(s); + expr.value = n; + expr.Eval(); + + Token t = expr.PopToken(); + ssassert(t.type == Token::Type::VALUE, "Expected a value"); + return t.value; +} + +//----------------------------------------------------------------------------- +// Gettext message keys +//----------------------------------------------------------------------------- + +class TranslationKey { +public: + bool hasContext; + std::string context; + std::string ident; +}; + +struct TranslationKeyLess { + bool operator()(const TranslationKey &a, const TranslationKey &b) const { + return a.hasContext < b.hasContext || + (a.hasContext == b.hasContext && a.context < b.context) || + (a.hasContext == b.hasContext && a.context == b.context && a.ident < b.ident); + } +}; + +//----------------------------------------------------------------------------- +// Gettext .po file parsing +//----------------------------------------------------------------------------- + +class GettextParser { +public: + ASCIIReader reader; + + unsigned pluralCount; + std::string pluralExpr; + + std::map, TranslationKeyLess> messages; + + void SkipSpace(); + std::string ReadString(); + void ParseHeader(const std::string &header); + void Parse(); +}; + +void GettextParser::SkipSpace() { + while(!reader.AtEnd()) { + if(reader.TryChar('#')) { + reader.SkipUntilEol(); + } else if(!reader.SkipSpace()) { + break; + } + } +} + +std::string GettextParser::ReadString() { + SkipSpace(); + reader.ExpectChar('"'); + + std::string result; + while(true) { + if(reader.AtEnd()) { + ssassert(false, "Unexpected EOF within a string"); + } else if(reader.TryChar('\"')) { + SkipSpace(); + if(!reader.TryChar('\"')) { + break; + } + } else if(reader.TryChar('\\')) { + if(reader.TryChar('\\')) { + result += '\\'; + } else if(reader.TryChar('n')) { + result += '\n'; + } else if(reader.TryChar('t')) { + result += '\t'; + } else if(reader.TryChar('"')) { + result += '"'; + } else { + ssassert(false, "Unexpected escape sequence"); + } + } else { + result += reader.ReadChar(); + } + } + return result; +} + +void GettextParser::ParseHeader(const std::string &header) { + ASCIIReader reader = ASCIIReader::From(header); + while(!reader.AtEnd()) { + reader.SkipSpace(); + if(reader.TryString("Plural-Forms:")) { + reader.SkipSpace(); + reader.ExpectString("nplurals="); + reader.SkipSpace(); + pluralCount = reader.ReadIntegerDecimal(); + reader.SkipSpace(); + reader.ExpectString(";"); + reader.SkipSpace(); + reader.ExpectString("plural="); + pluralExpr = reader.ReadUntilEol(); + } else { + reader.SkipUntilEol(); + } + } +} + +void GettextParser::Parse() { + // Default to a single form, in case a header is missing. + pluralCount = 1; + pluralExpr = "0"; + + SkipSpace(); + while(!reader.AtEnd()) { + TranslationKey key = {}; + + if(reader.TryString("msgctxt")) { + key.hasContext = true; + key.context = ReadString(); + } + + reader.ExpectString("msgid"); + key.ident = ReadString(); + + if(reader.TryString("msgid_plural")) { + ReadString(); // we don't need it + } + + std::vector msgstrs; + while(reader.TryString("msgstr")) { + if(reader.TryChar('[')) { + unsigned index = reader.ReadIntegerDecimal(); + reader.ExpectChar(']'); + if(msgstrs.size() <= index) { + msgstrs.resize(index + 1); + } + msgstrs[index] = ReadString(); + } else { + msgstrs.emplace_back(ReadString()); + break; + } + } + + if(key.ident.empty()) { + ssassert(msgstrs.size() == 1, + "Expected exactly one header msgstr"); + ParseHeader(msgstrs[0]); + } else { + ssassert(msgstrs.size() == 1 || + msgstrs.size() == pluralCount, + "Expected msgstr count to match plural form count"); + messages.emplace(key, msgstrs); + } + } +} + +//----------------------------------------------------------------------------- +// Translation management +//----------------------------------------------------------------------------- + +class Translation { +public: + unsigned pluralCount; + std::string pluralExpr; + + std::map, TranslationKeyLess> messages; + + static Translation From(const std::string &poData); + + const std::string &Translate(const TranslationKey &key); + const std::string &TranslatePlural(const TranslationKey &key, unsigned n); +}; + +Translation Translation::From(const std::string &poData) { + GettextParser parser = {}; + parser.reader = ASCIIReader::From(poData); + parser.Parse(); + + Translation trans = {}; + trans.pluralCount = parser.pluralCount; + trans.pluralExpr = parser.pluralExpr; + trans.messages = parser.messages; + return trans; +} + +const std::string &Translation::Translate(const TranslationKey &key) { + auto it = messages.find(key); + if(it == messages.end()) { + dbp("Missing (absent) translation for %s'%s'", key.context.c_str(), key.ident.c_str()); + messages[key].emplace_back(key.ident); + it = messages.find(key); + } + if(it->second[0].empty()) { + dbp("Missing (empty) translation for %s'%s'", key.context.c_str(), key.ident.c_str()); + it->second[0] = key.ident; + } + if(it->second.size() != 1) { + dbp("Incorrect use of translated message %s'%s'", key.context.c_str(), key.ident.c_str()); + ssassert(false, "Using a message with a plural form without a number"); + } + return it->second[0]; +} + +const std::string &Translation::TranslatePlural(const TranslationKey &key, unsigned n) { + unsigned pluralForm = PluralExpr::Eval(pluralExpr, n); + + auto it = messages.find(key); + if(it == messages.end()) { + dbp("Missing (absent) translation for %s'%s'", key.context.c_str(), key.ident.c_str()); + for(unsigned i = 0; i < pluralCount; i++) { + messages[key].emplace_back(key.ident); + } + it = messages.find(key); + } + if(it->second[pluralForm].empty()) { + dbp("Missing (empty) translation for %s'%s'[%d]", + key.context.c_str(), key.ident.c_str(), pluralForm); + it->second[pluralForm] = key.ident; + } + return it->second[pluralForm]; +} + +//----------------------------------------------------------------------------- +// Locale management +//----------------------------------------------------------------------------- + +static std::set locales; +static std::map translations; +static Translation dummyTranslation; +static Translation *currentTranslation = &dummyTranslation; + +const std::set &Locales() { + if(!locales.empty()) return locales; + + std::string localeList = LoadString("locales.txt"); + ASCIIReader reader = ASCIIReader::From(localeList); + while(!reader.AtEnd()) { + reader.SkipSpace(); + if(reader.TryChar('#')) { + reader.SkipUntilEol(); + continue; + } + + std::smatch m; + reader.ExpectRegex(std::regex("([a-z]{2})-([A-Z]{2}),([0-9A-F]{4}),(.+?)\n"), &m); + Locale locale = {}; + locale.language = m.str(1); + locale.region = m.str(2); + locale.lcid = std::stoi(m.str(3), NULL, 16); + locale.displayName = m.str(4); + locales.emplace(locale); + } + return locales; +} + +template +bool SetLocale(Predicate pred) { + auto it = std::find_if(Locales().begin(), Locales().end(), pred); + if(it != locales.end()) { + std::string filename = "locales/" + it->language + "_" + it->region + ".po"; + translations[*it] = Translation::From(LoadString(filename)); + currentTranslation = &translations[*it]; + return true; + } else { + return false; + } +} + +bool SetLocale(const std::string &name) { + return SetLocale([&](const Locale &locale) { + if(name == locale.language + "-" + locale.region) { + return true; + } else if(name == locale.language + "_" + locale.region) { + return true; + } else if(name == locale.language) { + return true; + } else { + return false; + } + }); +} + +bool SetLocale(uint16_t lcid) { + return SetLocale([&](const Locale &locale) { + return locale.lcid == lcid; + }); +} + +const std::string &Translate(const char *msgid) { + TranslationKey key = {}; + key.ident = msgid; + return currentTranslation->Translate(key); +} + +const std::string &Translate(const char *msgctxt, const char *msgid) { + TranslationKey key = {}; + key.hasContext = true; + key.context = msgctxt; + key.ident = msgid; + return currentTranslation->Translate(key); +} + +const std::string &TranslatePlural(const char *msgid, unsigned n) { + TranslationKey key = {}; + key.ident = msgid; + return currentTranslation->TranslatePlural(key, n); +} + +const std::string &TranslatePlural(const char *msgctxt, const char *msgid, unsigned n) { + TranslationKey key = {}; + key.hasContext = true; + key.context = msgctxt; + key.ident = msgid; + return currentTranslation->TranslatePlural(key, n); +} + +} diff --git a/src/resource.h b/src/resource.h new file mode 100644 index 0000000..35ed289 --- /dev/null +++ b/src/resource.h @@ -0,0 +1,127 @@ +//----------------------------------------------------------------------------- +// Discovery and loading of our resources (icons, fonts, templates, etc). +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- + +#ifndef SOLVESPACE_RESOURCE_H +#define SOLVESPACE_RESOURCE_H + +#include +#include +#include +#include +#include +#include + +namespace SolveSpace { + +class Camera; +class Point2d; +class Pixmap; +class Vector; +class RgbaColor; +namespace Platform { + class Path; +} // namespace Platform + +std::string LoadString(const std::string &name); +std::string LoadStringFromGzip(const std::string &name); +std::shared_ptr LoadPng(const std::string &name); + +class Pixmap { +public: + enum class Format { BGRA, RGBA, BGR, RGB, A }; + + Format format; + size_t width; + size_t height; + size_t stride; + std::vector data; + int colorType; + bool bgr; + + static std::shared_ptr Create(Format format, size_t width, size_t height); + static std::shared_ptr FromPng(const uint8_t *data, size_t size, bool flip = false); + + static std::shared_ptr ReadPng(FILE *f, bool flip = false); + static std::shared_ptr ReadPng(const Platform::Path &filename, bool flip = false); + bool WritePng(FILE *f, bool flip = false); + bool WritePng(const Platform::Path &filename, bool flip = false); + + size_t GetBytesPerPixel() const; + RgbaColor GetPixel(size_t x, size_t y) const; + bool Equals(const Pixmap &other) const; + + void ConvertTo(Format newFormat); + void SetPixel(size_t x, size_t y, RgbaColor color); + + std::shared_ptr Copy(); +}; + +class BitmapFont { +public: + struct Glyph { + uint8_t advanceCells; + uint16_t position; + }; + + std::string unifontData; + std::map glyphs; + std::shared_ptr texture; + bool textureUpdated; + uint16_t nextPosition; + + static BitmapFont From(std::string &&unifontData); + static BitmapFont Create(); + + bool IsEmpty() const { return unifontData.empty(); } + const Glyph &GetGlyph(char32_t codepoint); + void LocateGlyph(char32_t codepoint, double *s0, double *t0, double *s1, double *t1, + size_t *advanceWidth, size_t *boundingHeight); + + void AddGlyph(char32_t codepoint, std::shared_ptr pixmap); + + size_t GetWidth(char32_t codepoint); + size_t GetWidth(const std::string &str); +}; + +class VectorFont { +public: + struct Contour { + std::vector points; + }; + + struct Glyph { + std::vector contours; + double leftSideBearing; + double boundingWidth; + double advanceWidth; + }; + + std::string lffData; + std::map glyphs; + double rightSideBearing; + double capHeight; + double ascender; + double descender; + + static VectorFont From(std::string &&lffData); + static VectorFont *Builtin(); + + bool IsEmpty() const { return lffData.empty(); } + const Glyph &GetGlyph(char32_t codepoint); + + double GetCapHeight(double forCapHeight) const; + double GetHeight(double forCapHeight) const; + double GetWidth(double forCapHeight, const std::string &str); + Vector GetExtents(double forCapHeight, const std::string &str); + + void Trace(double forCapHeight, Vector o, Vector u, Vector v, const std::string &str, + const std::function &traceEdge); + void Trace(double forCapHeight, Vector o, Vector u, Vector v, const std::string &str, + const std::function &traceEdge, const Camera &camera); +}; + +} +#endif diff --git a/src/sketch.h b/src/sketch.h new file mode 100644 index 0000000..324d566 --- /dev/null +++ b/src/sketch.h @@ -0,0 +1,1002 @@ +//----------------------------------------------------------------------------- +// The parametric structure of our sketch, in multiple groups, that generate +// geometric entities and surfaces. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- + +#ifndef SOLVESPACE_SKETCH_H +#define SOLVESPACE_SKETCH_H + +#include +#include +#include +#include +#include + +#include "dsc.h" +#include "param.h" +#include "polygon.h" +#include "platform/platform.h" +#include "platform/gui.h" +#include "srf/surface.h" +#include "render/render.h" + +namespace SolveSpace { + +class hConstraint; + +class Expr; +class ExprVector; +class ExprQuaternion; + +enum class SolveResult : uint32_t; + +enum class PolyError : uint32_t { + GOOD = 0, + NOT_CLOSED = 1, + NOT_COPLANAR = 2, + SELF_INTERSECTING = 3, + ZERO_LEN_EDGE = 4 +}; + +enum class StipplePattern : uint32_t { + CONTINUOUS = 0, + SHORT_DASH = 1, + DASH = 2, + LONG_DASH = 3, + DASH_DOT = 4, + DASH_DOT_DOT = 5, + DOT = 6, + FREEHAND = 7, + ZIGZAG = 8, + + LAST = ZIGZAG +}; + +const std::vector &StipplePatternDashes(StipplePattern pattern); +double StipplePatternLength(StipplePattern pattern); + +enum class Command : uint32_t; + +// All of the hWhatever handles are a 32-bit ID, that is used to represent +// some data structure in the sketch. +class hEquation { +public: + uint32_t v; + + inline bool isFromConstraint() const; + inline hConstraint constraint() const; +}; + +template<> +struct IsHandleOracle : std::true_type {}; + +class hGroup { +public: + // bits 15: 0 -- group index + uint32_t v; + + inline hEntity entity(int i) const; + inline hParam param(int i) const; + inline hEquation equation(int i) const; +}; + +template<> +struct IsHandleOracle
: std::true_type {}; + +class hRequest { +public: + // bits 15: 0 -- request index + uint32_t v; + + inline hEntity entity(int i) const; + inline hParam param(int i) const; + + inline bool IsFromReferences() const; +}; + +template<> +struct IsHandleOracle : std::true_type {}; + +class hEntity { +public: + // bits 15: 0 -- entity index + // 31:16 -- request index + uint32_t v; + + inline bool isFromRequest() const; + inline hRequest request() const; + inline hGroup group() const; + inline hEquation equation(int i) const; +}; + +template<> +struct IsHandleOracle : std::true_type {}; + +class Equation { +public: + int tag; + hEquation h; + + Expr *e; + + void Clear() {} +}; + +class hStyle { +public: + uint32_t v; +}; + +template<> +struct IsHandleOracle : std::true_type {}; + +class Entity; +using EntityList = IdList; + +struct EntityId { + uint32_t v; // entity ID, starting from 0 +}; + +template<> +struct IsHandleOracle : std::true_type {}; + +struct EntityKey { + hEntity input; + int copyNumber; + // (input, copyNumber) gets mapped to hGroup::entity(i) +}; +struct EntityKeyHash { + size_t operator()(const EntityKey &k) const { + size_t h1 = std::hash{}(k.input.v), + h2 = std::hash{}(k.copyNumber); + return h1 ^ (h2 << 1); + } +}; +struct EntityKeyEqual { + bool operator()(const EntityKey &a, const EntityKey &b) const { + return std::tie(a.input, a.copyNumber) == std::tie(b.input, b.copyNumber); + } +}; +typedef std::unordered_map EntityMap; + +// A set of requests. Every request must have an associated group. +class Group { +public: + static const hGroup HGROUP_REFERENCES; + + int tag; + hGroup h; + + enum class CopyAs { + NUMERIC, + N_TRANS, + N_ROT_AA, + N_ROT_TRANS, + N_ROT_AXIS_TRANS, + }; + + enum class Type : uint32_t { + DRAWING_3D = 5000, + DRAWING_WORKPLANE = 5001, + EXTRUDE = 5100, + LATHE = 5101, + REVOLVE = 5102, + HELIX = 5103, + ROTATE = 5200, + TRANSLATE = 5201, + LINKED = 5300 + }; + Group::Type type; + + int order; + + hGroup opA; + hGroup opB; + bool visible; + bool suppress; + bool relaxConstraints; + bool allowRedundant; + bool suppressDofCalculation; + bool allDimsReference; + double scale; + + bool clean; + bool dofCheckOk; + hEntity activeWorkplane; + double valA; + double valB; + double valC; + RgbaColor color; + + struct { + SolveResult how; + int dof; + int findToFixTimeout; + bool timeout; + List remove; + } solved; + + enum class Subtype : uint32_t { + // For drawings in 2d + WORKPLANE_BY_POINT_ORTHO = 6000, + WORKPLANE_BY_LINE_SEGMENTS = 6001, + WORKPLANE_BY_POINT_NORMAL = 6002, + //WORKPLANE_BY_POINT_FACE = 6003, + //WORKPLANE_BY_FACE = 6004, + // For extrudes, translates, and rotates + ONE_SIDED = 7000, + TWO_SIDED = 7001, + ONE_SKEWED = 7004, + TWO_SKEWED = 7005 + }; + Group::Subtype subtype; + + bool skipFirst; // for step and repeat ops + + struct { + Quaternion q; + hEntity origin; + hEntity entityB; + hEntity entityC; + bool swapUV; + bool negateU; + bool negateV; + } predef; + + SPolygon polyLoops; + SBezierLoopSetSet bezierLoops; + SBezierLoopSet bezierOpens; + + struct { + PolyError how; + SEdge notClosedAt; + Vector errorPointAt; + } polyError; + + bool booleanFailed; + + SShell thisShell; + SShell runningShell; + + SMesh thisMesh; + SMesh runningMesh; + + bool displayDirty; + SMesh displayMesh; + SOutlineList displayOutlines; + + enum class CombineAs : uint32_t { + UNION = 0, + DIFFERENCE = 1, + ASSEMBLE = 2, + INTERSECTION = 3 + }; + CombineAs meshCombine; + + bool forceToMesh; + + EntityMap remap; + + Platform::Path linkFile; + SMesh impMesh; + SShell impShell; + EntityList impEntity; + + std::string name; + + + void Activate(); + std::string DescriptionString(); + void Clear(); + + static void AddParam(ParamList *param, hParam hp, double v); + void Generate(EntityList *entity, ParamList *param); + bool IsSolvedOkay(); + void TransformImportedBy(Vector t, Quaternion q); + bool IsTriangleMeshAssembly() const; + bool IsForcedToMeshBySource() const; + bool IsForcedToMesh() const; + // When a request generates entities from entities, and the source + // entities may have come from multiple requests, it's necessary to + // remap the entity ID so that it's still unique. We do this with a + // mapping list. + enum { + REMAP_LAST = 1000, + REMAP_TOP = 1001, + REMAP_BOTTOM = 1002, + REMAP_PT_TO_LINE = 1003, + REMAP_LINE_TO_FACE = 1004, + REMAP_LATHE_START = 1006, + REMAP_LATHE_END = 1007, + REMAP_PT_TO_ARC = 1008, + REMAP_PT_TO_NORMAL = 1009, + REMAP_LATHE_ARC_CENTER = 1010, + }; + hEntity Remap(hEntity in, int copyNumber); + void MakeExtrusionLines(EntityList *el, hEntity in); + void MakeLatheCircles(EntityList *el, ParamList *param, hEntity in, Vector pt, Vector axis); + void MakeLatheSurfacesSelectable(EntityList *el, hEntity in, Vector axis); + void MakeRevolveEndFaces(EntityList *el, hEntity pt, int ai, int af); + void MakeExtrusionTopBottomFaces(EntityList *el, hEntity pt); + void CopyEntity(EntityList *el, + Entity *ep, int timesApplied, int remap, + hParam dx, hParam dy, hParam dz, + hParam qw, hParam qvx, hParam qvy, hParam qvz, hParam dist, + CopyAs as); + + void AddEq(IdList *l, Expr *expr, int index); + void GenerateEquations(IdList *l); + bool IsVisible(); + size_t GetNumConstraints(); + Vector ExtrusionGetVector(); + void ExtrusionForceVectorTo(const Vector &v); + + // Assembling the curves into loops, and into a piecewise linear polygon + // at the same time. + void AssembleLoops(bool *allClosed, bool *allCoplanar, bool *allNonZeroLen); + void GenerateLoops(); + // And the mesh stuff + Group *PreviousGroup() const; + Group *RunningMeshGroup() const; + bool IsMeshGroup(); + + void GenerateShellAndMesh(); + template void GenerateForStepAndRepeat(T *steps, T *outs, Group::CombineAs forWhat); + template void GenerateForBoolean(T *a, T *b, T *o, Group::CombineAs how); + void GenerateDisplayItems(); + + enum class DrawMeshAs { DEFAULT, HOVERED, SELECTED }; + void DrawMesh(DrawMeshAs how, Canvas *canvas); + void Draw(Canvas *canvas); + void DrawPolyError(Canvas *canvas); + void DrawFilledPaths(Canvas *canvas); + void DrawContourAreaLabels(Canvas *canvas); + bool ShouldDrawExploded() const; + + SPolygon GetPolygon(); + + static void MenuGroup(Command id); + static void MenuGroup(Command id, Platform::Path linkFile); +}; + +// A user request for some primitive or derived operation; for example a +// line, or a step and repeat. +class Request { +public: + // Some predefined requests, that are present in every sketch. + static const hRequest HREQUEST_REFERENCE_XY; + static const hRequest HREQUEST_REFERENCE_YZ; + static const hRequest HREQUEST_REFERENCE_ZX; + + int tag; + hRequest h; + + // Types of requests + enum class Type : uint32_t { + WORKPLANE = 100, + DATUM_POINT = 101, + LINE_SEGMENT = 200, + CUBIC = 300, + CUBIC_PERIODIC = 301, + CIRCLE = 400, + ARC_OF_CIRCLE = 500, + TTF_TEXT = 600, + IMAGE = 700 + }; + + Request::Type type; + int extraPoints; + + hEntity workplane; // or Entity::FREE_IN_3D + hGroup group; + hStyle style; + + bool construction; + + std::string str; + std::string font; + Platform::Path file; + double aspectRatio; + int groupRequestIndex; + + static hParam AddParam(ParamList *param, hParam hp); + void Generate(EntityList *entity, ParamList *param); + + std::string DescriptionString() const; + int IndexOfPoint(hEntity he) const; + + void Clear() {} +}; + +#define MAX_POINTS_IN_ENTITY (12) +class EntityBase { +public: + int tag; + hEntity h; + + static const hEntity FREE_IN_3D; + static const hEntity NO_ENTITY; + + enum class Type : uint32_t { + POINT_IN_3D = 2000, + POINT_IN_2D = 2001, + POINT_N_TRANS = 2010, + POINT_N_ROT_TRANS = 2011, + POINT_N_COPY = 2012, + POINT_N_ROT_AA = 2013, + POINT_N_ROT_AXIS_TRANS = 2014, + + NORMAL_IN_3D = 3000, + NORMAL_IN_2D = 3001, + NORMAL_N_COPY = 3010, + NORMAL_N_ROT = 3011, + NORMAL_N_ROT_AA = 3012, + + DISTANCE = 4000, + DISTANCE_N_COPY = 4001, + + FACE_NORMAL_PT = 5000, + FACE_XPROD = 5001, + FACE_N_ROT_TRANS = 5002, + FACE_N_TRANS = 5003, + FACE_N_ROT_AA = 5004, + FACE_ROT_NORMAL_PT = 5005, + FACE_N_ROT_AXIS_TRANS = 5006, + + WORKPLANE = 10000, + LINE_SEGMENT = 11000, + CUBIC = 12000, + CUBIC_PERIODIC = 12001, + CIRCLE = 13000, + ARC_OF_CIRCLE = 14000, + TTF_TEXT = 15000, + IMAGE = 16000 + }; + + Type type; + + hGroup group; + hEntity workplane; // or Entity::FREE_IN_3D + + // When it comes time to draw an entity, we look here to get the + // defining variables. + hEntity point[MAX_POINTS_IN_ENTITY]; + int extraPoints; + hEntity normal; + hEntity distance; + // The only types that have their own params are points, normals, + // and directions. + hParam param[8]; + + // Transformed points/normals/distances have their numerical base + Vector numPoint; + Quaternion numNormal; + double numDistance; + + std::string str; + std::string font; + Platform::Path file; + double aspectRatio; + + // For entities that are derived by a transformation, the number of + // times to apply the transformation. + int timesApplied; + + Quaternion GetAxisAngleQuaternion(int param0) const; + ExprQuaternion GetAxisAngleQuaternionExprs(int param0) const; + + bool IsCircle() const; + Expr *CircleGetRadiusExpr() const; + double CircleGetRadiusNum() const; + void ArcGetAngles(double *thetaa, double *thetab, double *dtheta) const; + + bool HasVector() const; + ExprVector VectorGetExprs() const; + ExprVector VectorGetExprsInWorkplane(hEntity wrkpl) const; + Vector VectorGetNum() const; + Vector VectorGetRefPoint() const; + Vector VectorGetStartPoint() const; + + // For distances + bool IsDistance() const; + double DistanceGetNum() const; + Expr *DistanceGetExpr() const; + void DistanceForceTo(double v); + + bool IsWorkplane() const; + // The plane is points P such that P dot (xn, yn, zn) - d = 0 + void WorkplaneGetPlaneExprs(ExprVector *n, Expr **d) const; + ExprVector WorkplaneGetOffsetExprs() const; + Vector WorkplaneGetOffset() const; + EntityBase *Normal() const; + + bool IsFace() const; + ExprVector FaceGetNormalExprs() const; + Vector FaceGetNormalNum() const; + ExprVector FaceGetPointExprs() const; + Vector FaceGetPointNum() const; + + bool IsPoint() const; + // Applies for any of the point types + Vector PointGetNum() const; + ExprVector PointGetExprs() const; + void PointGetExprsInWorkplane(hEntity wrkpl, Expr **u, Expr **v) const; + ExprVector PointGetExprsInWorkplane(hEntity wrkpl) const; + void PointForceTo(Vector v); + void PointForceParamTo(Vector v); + // These apply only the POINT_N_ROT_TRANS, which has an assoc rotation + Quaternion PointGetQuaternion() const; + void PointForceQuaternionTo(Quaternion q); + + bool IsNormal() const; + // Applies for any of the normal types + Quaternion NormalGetNum() const; + ExprQuaternion NormalGetExprs() const; + void NormalForceTo(Quaternion q); + + Vector NormalU() const; + Vector NormalV() const; + Vector NormalN() const; + ExprVector NormalExprsU() const; + ExprVector NormalExprsV() const; + ExprVector NormalExprsN() const; + + Vector CubicGetStartNum() const; + Vector CubicGetFinishNum() const; + ExprVector CubicGetStartTangentExprs() const; + ExprVector CubicGetFinishTangentExprs() const; + Vector CubicGetStartTangentNum() const; + Vector CubicGetFinishTangentNum() const; + + bool HasEndpoints() const; + Vector EndpointStart() const; + Vector EndpointFinish() const; + bool IsInPlane(Vector norm, double distance) const; + + void RectGetPointsExprs(ExprVector *eap, ExprVector *ebp) const; + + void AddEq(IdList *l, Expr *expr, int index) const; + void GenerateEquations(IdList *l) const; + + void Clear() {} +}; + +class Entity : public EntityBase { +public: + // Necessary for Entity e = {} to zero-initialize, since + // classes with base classes are not aggregates and + // the default constructor does not initialize members. + // + // Note EntityBase({}); without explicitly value-initializing + // the base class, MSVC2013 will default-initialize it, leaving + // POD members with indeterminate value. + Entity() : EntityBase({}), forceHidden(), actPoint(), actNormal(), + actDistance(), actVisible(), style(), construction(), + beziers(), edges(), edgesChordTol(), screenBBox(), screenBBoxValid() {}; + + // A linked entity that was hidden in the source file ends up hidden + // here too. + bool forceHidden; + + // All points/normals/distances have their numerical value; this is + // a convenience, to simplify the link/assembly code, so that the + // part is entirely described by the entities. + Vector actPoint; + Quaternion actNormal; + double actDistance; + // and the shown state also gets saved here, for later import + bool actVisible; + + hStyle style; + bool construction; + + SBezierList beziers; + SEdgeList edges; + double edgesChordTol; + BBox screenBBox; + bool screenBBoxValid; + + bool IsStylable() const; + bool IsVisible() const; + bool CanBeDragged() const; + + enum class DrawAs { DEFAULT, OVERLAY, HIDDEN, HOVERED, SELECTED }; + void Draw(DrawAs how, Canvas *canvas); + void GetReferencePoints(std::vector *refs); + int GetPositionOfPoint(const Camera &camera, Point2d p); + + void ComputeInterpolatingSpline(SBezierList *sbl, bool periodic) const; + void GenerateBezierCurves(SBezierList *sbl) const; + void GenerateEdges(SEdgeList *el); + + SBezierList *GetOrGenerateBezierCurves(); + SEdgeList *GetOrGenerateEdges(); + BBox GetOrGenerateScreenBBox(bool *hasBBox); + + void CalculateNumerical(bool forExport); + + std::string DescriptionString() const; + + void Clear() { + beziers.l.Clear(); + edges.l.Clear(); + } + + bool ShouldDrawExploded() const; + Vector ExplodeOffset() const; + Vector PointGetDrawNum() const; +}; + +class EntReqTable { +public: + static void GetRequestInfo(Request::Type req, int extraPoints, + EntityBase::Type *ent, int *pts, bool *hasNormal, bool *hasDistance); + static bool GetEntityInfo(EntityBase::Type ent, int extraPoints, + Request::Type *req, int *pts, bool *hasNormal, bool *hasDistance); + static Request::Type GetRequestForEntity(EntityBase::Type ent); +}; + +class hConstraint { +public: + uint32_t v; + + inline hEquation equation(int i) const; + inline hParam param(int i) const; +}; + +template<> +struct IsHandleOracle : std::true_type {}; + +class ConstraintBase { +public: + int tag; + hConstraint h; + + static const hConstraint NO_CONSTRAINT; + + enum class Type : uint32_t { + POINTS_COINCIDENT = 20, + PT_PT_DISTANCE = 30, + PT_PLANE_DISTANCE = 31, + PT_LINE_DISTANCE = 32, + PT_FACE_DISTANCE = 33, + PROJ_PT_DISTANCE = 34, + PT_IN_PLANE = 41, + PT_ON_LINE = 42, + PT_ON_FACE = 43, + EQUAL_LENGTH_LINES = 50, + LENGTH_RATIO = 51, + EQ_LEN_PT_LINE_D = 52, + EQ_PT_LN_DISTANCES = 53, + EQUAL_ANGLE = 54, + EQUAL_LINE_ARC_LEN = 55, + LENGTH_DIFFERENCE = 56, + SYMMETRIC = 60, + SYMMETRIC_HORIZ = 61, + SYMMETRIC_VERT = 62, + SYMMETRIC_LINE = 63, + AT_MIDPOINT = 70, + HORIZONTAL = 80, + VERTICAL = 81, + DIAMETER = 90, + PT_ON_CIRCLE = 100, + SAME_ORIENTATION = 110, + ANGLE = 120, + PARALLEL = 121, + PERPENDICULAR = 122, + ARC_LINE_TANGENT = 123, + CUBIC_LINE_TANGENT = 124, + CURVE_CURVE_TANGENT = 125, + EQUAL_RADIUS = 130, + WHERE_DRAGGED = 200, + ARC_ARC_LEN_RATIO = 210, + ARC_LINE_LEN_RATIO = 211, + ARC_ARC_DIFFERENCE = 212, + ARC_LINE_DIFFERENCE = 213, + COMMENT = 1000 + }; + + Type type; + + hGroup group; + hEntity workplane; + + // These are the parameters for the constraint. + double valA; + hParam valP; + hEntity ptA; + hEntity ptB; + hEntity entityA; + hEntity entityB; + hEntity entityC; + hEntity entityD; + bool other; + bool other2; + + bool reference; // a ref dimension, that generates no eqs + std::string comment; // since comments are represented as constraints + + bool Equals(const ConstraintBase &c) const { + return type == c.type && group == c.group && workplane == c.workplane && + valA == c.valA && valP == c.valP && ptA == c.ptA && ptB == c.ptB && + entityA == c.entityA && entityB == c.entityB && + entityC == c.entityC && entityD == c.entityD && + other == c.other && other2 == c.other2 && reference == c.reference && + comment == c.comment; + } + + bool HasLabel() const; + bool IsProjectible() const; + + void Generate(ParamList *param); + + void GenerateEquations(IdList *entity, + bool forReference = false) const; + // Some helpers when generating symbolic constraint equations + void ModifyToSatisfy(); + void AddEq(IdList *l, Expr *expr, int index) const; + void AddEq(IdList *l, const ExprVector &v, int baseIndex = 0) const; + static Expr *DirectionCosine(hEntity wrkpl, ExprVector ae, ExprVector be); + static Expr *Distance(hEntity workplane, hEntity pa, hEntity pb); + static Expr *PointLineDistance(hEntity workplane, hEntity pt, hEntity ln); + static Expr *PointPlaneDistance(ExprVector p, hEntity plane); + static ExprVector VectorsParallel3d(ExprVector a, ExprVector b, hParam p); + static ExprVector PointInThreeSpace(hEntity workplane, Expr *u, Expr *v); + + void Clear() {} +}; + +class Constraint : public ConstraintBase { +public: + // See Entity::Entity(). + Constraint() : ConstraintBase({}), disp() {} + + // These define how the constraint is drawn on-screen. + struct { + Vector offset; + hStyle style; + } disp; + + bool IsVisible() const; + bool IsStylable() const; + hStyle GetStyle() const; + bool HasLabel() const; + std::string Label() const; + + enum class DrawAs { DEFAULT, HOVERED, SELECTED }; + void Draw(DrawAs how, Canvas *canvas); + Vector GetLabelPos(const Camera &camera); + void GetReferencePoints(const Camera &camera, std::vector *refs); + + void DoLayout(DrawAs how, Canvas *canvas, + Vector *labelPos, std::vector *refs); + void DoLine(Canvas *canvas, Canvas::hStroke hcs, Vector a, Vector b); + void DoStippledLine(Canvas *canvas, Canvas::hStroke hcs, Vector a, Vector b); + bool DoLineExtend(Canvas *canvas, Canvas::hStroke hcs, + Vector p0, Vector p1, Vector pt, double salient); + void DoArcForAngle(Canvas *canvas, Canvas::hStroke hcs, + Vector a0, Vector da, Vector b0, Vector db, + Vector offset, Vector *ref, bool trim, Vector explodeOffset); + void DoArrow(Canvas *canvas, Canvas::hStroke hcs, + Vector p, Vector dir, Vector n, double width, double angle, double da); + void DoLineWithArrows(Canvas *canvas, Canvas::hStroke hcs, + Vector ref, Vector a, Vector b, bool onlyOneExt); + int DoLineTrimmedAgainstBox(Canvas *canvas, Canvas::hStroke hcs, + Vector ref, Vector a, Vector b, bool extend, + Vector gr, Vector gu, double swidth, double sheight); + int DoLineTrimmedAgainstBox(Canvas *canvas, Canvas::hStroke hcs, + Vector ref, Vector a, Vector b, bool extend = true); + void DoLabel(Canvas *canvas, Canvas::hStroke hcs, + Vector ref, Vector *labelPos, Vector gr, Vector gu); + void DoProjectedPoint(Canvas *canvas, Canvas::hStroke hcs, Vector *p); + void DoProjectedPoint(Canvas *canvas, Canvas::hStroke hcs, Vector *p, Vector n, Vector o); + + void DoEqualLenTicks(Canvas *canvas, Canvas::hStroke hcs, + Vector a, Vector b, Vector gn, Vector *refp); + void DoEqualRadiusTicks(Canvas *canvas, Canvas::hStroke hcs, + hEntity he, Vector *refp); + + std::string DescriptionString() const; + + bool ShouldDrawExploded() const; + + static hConstraint AddConstraint(Constraint *c, bool rememberForUndo = true); + static void MenuConstrain(Command id); + static void DeleteAllConstraintsFor(Constraint::Type type, hEntity entityA, hEntity ptA); + + static hConstraint ConstrainCoincident(hEntity ptA, hEntity ptB); + static hConstraint Constrain(Constraint::Type type, hEntity ptA, hEntity ptB, hEntity entityA, + hEntity entityB = Entity::NO_ENTITY, bool other = false, + bool other2 = false); + static hConstraint TryConstrain(Constraint::Type type, hEntity ptA, hEntity ptB, + hEntity entityA, hEntity entityB = Entity::NO_ENTITY, + bool other = false, bool other2 = false); + static bool ConstrainArcLineTangent(Constraint *c, Entity *line, Entity *arc, + Entity *arcendpoint); + static bool ConstrainCubicLineTangent(Constraint *c, Entity *line, Entity *cubic, + Entity *curveendpoint); + static bool ConstrainCurveCurveTangent(Constraint *c, Entity *eA, Entity *eB, Entity *p1, + Entity *p2); +}; + +class Style { +public: + int tag; + hStyle h; + + enum { + // If an entity has no style, then it will be colored according to + // whether the group that it's in is active or not, whether it's + // construction or not, and so on. + NO_STYLE = 0, + + ACTIVE_GRP = 1, + CONSTRUCTION = 2, + INACTIVE_GRP = 3, + DATUM = 4, + SOLID_EDGE = 5, + CONSTRAINT = 6, + SELECTED = 7, + HOVERED = 8, + CONTOUR_FILL = 9, + NORMALS = 10, + ANALYZE = 11, + DRAW_ERROR = 12, + DIM_SOLID = 13, + HIDDEN_EDGE = 14, + OUTLINE = 15, + + FIRST_CUSTOM = 0x100 + }; + + std::string name; + + enum class UnitsAs : uint32_t { + PIXELS = 0, + MM = 1 + }; + double width; + UnitsAs widthAs; + double textHeight; + UnitsAs textHeightAs; + enum class TextOrigin : uint32_t { + NONE = 0x00, + LEFT = 0x01, + RIGHT = 0x02, + BOT = 0x04, + TOP = 0x08 + }; + TextOrigin textOrigin; + double textAngle; + RgbaColor color; + bool filled; + RgbaColor fillColor; + bool visible; + bool exportable; + StipplePattern stippleType; + double stippleScale; + int zIndex; + + // The default styles, for entities that don't have a style assigned yet, + // and for datums and such. + typedef struct { + hStyle h; + const char *cnfPrefix; + RgbaColor color; + double width; + int zIndex; + bool exportable; + StipplePattern stippleType; + } Default; + static const Default Defaults[]; + + static std::string CnfColor(const std::string &prefix); + static std::string CnfWidth(const std::string &prefix); + static std::string CnfStippleType(const std::string &prefix); + static std::string CnfStippleScale(const std::string &prefix); + static std::string CnfTextHeight(const std::string &prefix); + static std::string CnfPrefixToName(const std::string &prefix); + static std::string CnfExportable(const std::string &prefix); + + static void CreateAllDefaultStyles(); + static void CreateDefaultStyle(hStyle h); + static void FillDefaultStyle(Style *s, const Default *d = NULL, bool factory = false); + static void FreezeDefaultStyles(Platform::SettingsRef settings); + static void LoadFactoryDefaults(); + + static void AssignSelectionToStyle(uint32_t v); + static uint32_t CreateCustomStyle(bool rememberForUndo = true); + + static RgbaColor RewriteColor(RgbaColor rgb); + + static Style *Get(hStyle hs); + static RgbaColor Color(hStyle hs, bool forExport=false); + static RgbaColor Color(int hs, bool forExport=false); + static RgbaColor FillColor(hStyle hs, bool forExport=false); + static double Width(hStyle hs); + static double Width(int hs); + static double WidthMm(int hs); + static double TextHeight(hStyle hs); + static double DefaultTextHeight(); + static Canvas::Stroke Stroke(hStyle hs); + static Canvas::Stroke Stroke(int hs); + static bool Exportable(int hs); + static hStyle ForEntity(hEntity he); + static StipplePattern PatternType(hStyle hs); + static double StippleScale(hStyle hs); + static double StippleScaleMm(hStyle hs); + static std::string StipplePatternName(hStyle hs); + static std::string StipplePatternName(StipplePattern stippleType); + static StipplePattern StipplePatternFromString(std::string name); + + std::string DescriptionString() const; + + void Clear() {} +}; + + +inline hEntity hGroup::entity(int i) const + { hEntity r; r.v = 0x80000000 | (v << 16) | (uint32_t)i; return r; } +inline hParam hGroup::param(int i) const + { hParam r; r.v = 0x80000000 | (v << 16) | (uint32_t)i; return r; } +inline hEquation hGroup::equation(int i) const + { hEquation r; r.v = (v << 16) | 0x80000000 | (uint32_t)i; return r; } + +inline bool hRequest::IsFromReferences() const { + if(*this == Request::HREQUEST_REFERENCE_XY) return true; + if(*this == Request::HREQUEST_REFERENCE_YZ) return true; + if(*this == Request::HREQUEST_REFERENCE_ZX) return true; + return false; +} +inline hEntity hRequest::entity(int i) const + { hEntity r; r.v = (v << 16) | (uint32_t)i; return r; } +inline hParam hRequest::param(int i) const + { hParam r; r.v = (v << 16) | (uint32_t)i; return r; } + +inline bool hEntity::isFromRequest() const + { if(v & 0x80000000) return false; else return true; } +inline hRequest hEntity::request() const + { hRequest r; r.v = (v >> 16); return r; } +inline hGroup hEntity::group() const + { hGroup r; r.v = (v >> 16) & 0x3fff; return r; } +inline hEquation hEntity::equation(int i) const + { hEquation r; r.v = v | 0x40000000 | (uint32_t)i; return r; } + +inline hRequest hParam::request() const + { hRequest r; r.v = (v >> 16); return r; } + + +inline hEquation hConstraint::equation(int i) const + { hEquation r; r.v = (v << 16) | (uint32_t)i; return r; } +inline hParam hConstraint::param(int i) const + { hParam r; r.v = v | 0x40000000 | (uint32_t)i; return r; } + +inline bool hEquation::isFromConstraint() const + { if(v & 0xc0000000) return false; else return true; } +inline hConstraint hEquation::constraint() const + { hConstraint r; r.v = (v >> 16); return r; } + +// The format for entities stored on the clipboard. +class ClipboardRequest { +public: + Request::Type type; + int extraPoints; + hStyle style; + std::string str; + std::string font; + Platform::Path file; + bool construction; + + Vector point[MAX_POINTS_IN_ENTITY]; + double distance; + + hEntity oldEnt; + hEntity oldPointEnt[MAX_POINTS_IN_ENTITY]; + hRequest newReq; +}; + +} // namespace SolveSpace + +#endif diff --git a/src/slvs/CMakeLists.txt b/src/slvs/CMakeLists.txt new file mode 100644 index 0000000..d931f8f --- /dev/null +++ b/src/slvs/CMakeLists.txt @@ -0,0 +1,80 @@ +include(GNUInstallDirs) + +# libslvs +add_library(slvs-interface INTERFACE) +target_sources(slvs-interface INTERFACE lib.cpp) +target_compile_definitions(slvs-interface INTERFACE -DLIBRARY) +target_include_directories(slvs-interface INTERFACE ${CMAKE_SOURCE_DIR}/include) +target_link_libraries(slvs-interface INTERFACE slvs-solver mimalloc-static) + +if(ENABLE_PYTHON_LIB) + add_custom_command( + OUTPUT cylib.c + COMMAND cython lib.pyx -o ${CMAKE_CURRENT_BINARY_DIR}/cylib.c --include-dir ${CMAKE_SOURCE_DIR}/include --module-name solvespace + DEPENDS lib.pyx + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + find_package(Python REQUIRED COMPONENTS Interpreter Development.Module) + Python_add_library(slvs-py MODULE cylib.c) + set_target_properties(slvs-py PROPERTIES POSITION_INDEPENDENT_CODE ON) + target_compile_definitions(slvs-py PRIVATE -DSTATIC_LIB) + target_link_libraries( + slvs-py + PRIVATE + slvs-interface + Python::Module + ) + set_target_properties(slvs-py PROPERTIES LIBRARY_OUTPUT_NAME solvespace) + if(SKBUILD) + install(TARGETS slvs-py + DESTINATION ${SKBUILD_PROJECT_NAME} + COMPONENT python + ) + else() + install(TARGETS slvs-py + DESTINATION "slvs" + COMPONENT python + ) + install( + DIRECTORY ${CMAKE_SOURCE_DIR}/python/slvs/ + DESTINATION "slvs" + COMPONENT python + ) + endif() +endif() + +if(EMSCRIPTEN) + add_executable(slvs-wasm jslib.cpp) + target_compile_features(slvs-wasm PRIVATE cxx_std_17) + target_link_libraries(slvs-wasm PRIVATE slvs-interface embind) + target_link_options(slvs-wasm PRIVATE + "SHELL:-s MODULARIZE=1" + "SHELL:-s EXPORT_NAME=solvespace" + "SHELL:-s SINGLE_FILE=1" + "SHELL:-s INITIAL_MEMORY=512MB" + "SHELL:-s ALLOW_MEMORY_GROWTH" + -O3 + -flto + --closure 1) + set_target_properties(slvs-wasm PROPERTIES OUTPUT_NAME "slvs" SUFFIX ".js") +endif() + +add_library(slvs SHARED) +target_compile_definitions(slvs PRIVATE -DEXPORT_DLL) +target_link_libraries(slvs PRIVATE slvs-interface) +set_target_properties(slvs PROPERTIES POSITION_INDEPENDENT_CODE ON) +target_include_directories(slvs PUBLIC ${CMAKE_SOURCE_DIR}/include) + +set_target_properties(slvs PROPERTIES + PUBLIC_HEADER ${CMAKE_SOURCE_DIR}/include/slvs.h + VERSION ${PROJECT_VERSION} + SOVERSION 1 +) + +# if(NOT WIN32) +install(TARGETS slvs + COMPONENT slvs + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +# endif() diff --git a/src/slvs/jslib.cpp b/src/slvs/jslib.cpp new file mode 100644 index 0000000..82074ff --- /dev/null +++ b/src/slvs/jslib.cpp @@ -0,0 +1,210 @@ +#include "slvs.h" +#include + +#include "solvespace.h" + +using namespace SolveSpace; + +struct JsSolveResult { + int result; + int dof; + size_t nbad; + emscripten::val bad; +}; + +static JsSolveResult solveSketch(Slvs_hGroup g, bool calculateFaileds) { + JsSolveResult jsResult = {}; + Slvs_hConstraint *c = nullptr; + Slvs_SolveResult ret = Slvs_SolveSketch(g, calculateFaileds ? &c : nullptr); + if(c) { + jsResult.bad = emscripten::val::global("Uint32Array").new_(ret.nbad); + jsResult.bad.call("set", emscripten::typed_memory_view(ret.nbad, c)); + free(c); + } + jsResult.result = ret.result; + jsResult.dof = ret.dof; + jsResult.nbad = ret.nbad; + return jsResult; +} + +EMSCRIPTEN_BINDINGS(slvs) { + emscripten::constant("C_POINTS_COINCIDENT", SLVS_C_POINTS_COINCIDENT); + emscripten::constant("C_PT_PT_DISTANCE", SLVS_C_PT_PT_DISTANCE); + emscripten::constant("C_PT_PLANE_DISTANCE", SLVS_C_PT_PLANE_DISTANCE); + emscripten::constant("C_PT_LINE_DISTANCE", SLVS_C_PT_LINE_DISTANCE); + emscripten::constant("C_PT_FACE_DISTANCE", SLVS_C_PT_FACE_DISTANCE); + emscripten::constant("C_PT_IN_PLANE", SLVS_C_PT_IN_PLANE); + emscripten::constant("C_PT_ON_LINE", SLVS_C_PT_ON_LINE); + emscripten::constant("C_PT_ON_FACE", SLVS_C_PT_ON_FACE); + emscripten::constant("C_EQUAL_LENGTH_LINES", SLVS_C_EQUAL_LENGTH_LINES); + emscripten::constant("C_LENGTH_RATIO", SLVS_C_LENGTH_RATIO); + emscripten::constant("C_EQ_LEN_PT_LINE_D", SLVS_C_EQ_LEN_PT_LINE_D); + emscripten::constant("C_EQ_PT_LN_DISTANCES", SLVS_C_EQ_PT_LN_DISTANCES); + emscripten::constant("C_EQUAL_ANGLE", SLVS_C_EQUAL_ANGLE); + emscripten::constant("C_EQUAL_LINE_ARC_LEN", SLVS_C_EQUAL_LINE_ARC_LEN); + emscripten::constant("C_SYMMETRIC", SLVS_C_SYMMETRIC); + emscripten::constant("C_SYMMETRIC_HORIZ", SLVS_C_SYMMETRIC_HORIZ); + emscripten::constant("C_SYMMETRIC_VERT", SLVS_C_SYMMETRIC_VERT); + emscripten::constant("C_SYMMETRIC_LINE", SLVS_C_SYMMETRIC_LINE); + emscripten::constant("C_AT_MIDPOINT", SLVS_C_AT_MIDPOINT); + emscripten::constant("C_HORIZONTAL", SLVS_C_HORIZONTAL); + emscripten::constant("C_VERTICAL", SLVS_C_VERTICAL); + emscripten::constant("C_DIAMETER", SLVS_C_DIAMETER); + emscripten::constant("C_PT_ON_CIRCLE", SLVS_C_PT_ON_CIRCLE); + emscripten::constant("C_SAME_ORIENTATION", SLVS_C_SAME_ORIENTATION); + emscripten::constant("C_ANGLE", SLVS_C_ANGLE); + emscripten::constant("C_PARALLEL", SLVS_C_PARALLEL); + emscripten::constant("C_PERPENDICULAR", SLVS_C_PERPENDICULAR); + emscripten::constant("C_ARC_LINE_TANGENT", SLVS_C_ARC_LINE_TANGENT); + emscripten::constant("C_CUBIC_LINE_TANGENT", SLVS_C_CUBIC_LINE_TANGENT); + emscripten::constant("C_EQUAL_RADIUS", SLVS_C_EQUAL_RADIUS); + emscripten::constant("C_PROJ_PT_DISTANCE", SLVS_C_PROJ_PT_DISTANCE); + emscripten::constant("C_WHERE_DRAGGED", SLVS_C_WHERE_DRAGGED); + emscripten::constant("C_CURVE_CURVE_TANGENT", SLVS_C_CURVE_CURVE_TANGENT); + emscripten::constant("C_LENGTH_DIFFERENCE", SLVS_C_LENGTH_DIFFERENCE); + emscripten::constant("C_ARC_ARC_LEN_RATIO", SLVS_C_ARC_ARC_LEN_RATIO); + emscripten::constant("C_ARC_LINE_LEN_RATIO", SLVS_C_ARC_LINE_LEN_RATIO); + emscripten::constant("C_ARC_ARC_DIFFERENCE", SLVS_C_ARC_ARC_DIFFERENCE); + emscripten::constant("C_ARC_LINE_DIFFERENCE", SLVS_C_ARC_LINE_DIFFERENCE); + + emscripten::constant("E_POINT_IN_3D", SLVS_E_POINT_IN_3D); + emscripten::constant("E_POINT_IN_2D", SLVS_E_POINT_IN_2D); + emscripten::constant("E_NORMAL_IN_3D", SLVS_E_NORMAL_IN_3D); + emscripten::constant("E_NORMAL_IN_2D", SLVS_E_NORMAL_IN_2D); + emscripten::constant("E_DISTANCE", SLVS_E_DISTANCE); + emscripten::constant("E_WORKPLANE", SLVS_E_WORKPLANE); + emscripten::constant("E_LINE_SEGMENT", SLVS_E_LINE_SEGMENT); + emscripten::constant("E_CUBIC", SLVS_E_CUBIC); + emscripten::constant("E_CIRCLE", SLVS_E_CIRCLE); + emscripten::constant("E_ARC_OF_CIRCLE", SLVS_E_ARC_OF_CIRCLE); + + emscripten::constant("E_NONE", SLVS_E_NONE); + emscripten::constant("E_FREE_IN_3D", SLVS_E_FREE_IN_3D); + + emscripten::constant("RESULT_OKAY", SLVS_RESULT_OKAY); + emscripten::constant("RESULT_INCONSISTENT", SLVS_RESULT_INCONSISTENT); + emscripten::constant("RESULT_DIDNT_CONVERGE", SLVS_RESULT_DIDNT_CONVERGE); + emscripten::constant("RESULT_TOO_MANY_UNKNOWNS", SLVS_RESULT_TOO_MANY_UNKNOWNS); + emscripten::constant("RESULT_REDUNDANT_OKAY", SLVS_RESULT_REDUNDANT_OKAY); + + emscripten::value_array>("array_uint32_4") + .element(emscripten::index<0>()) + .element(emscripten::index<1>()) + .element(emscripten::index<3>()) + .element(emscripten::index<4>()); + + emscripten::value_object("Slvs_Entity") + .field("h", &Slvs_Entity::h) + .field("group", &Slvs_Entity::group) + .field("type", &Slvs_Entity::type) + .field("wrkpl", &Slvs_Entity::wrkpl) + .field("point", &Slvs_Entity::point) + .field("normal", &Slvs_Entity::normal) + .field("distance", &Slvs_Entity::distance) + .field("param", &Slvs_Entity::param); + + emscripten::value_object("Slvs_Constraint") + .field("h", &Slvs_Constraint::h) + .field("group", &Slvs_Constraint::group) + .field("type", &Slvs_Constraint::type) + .field("wrkpl", &Slvs_Constraint::wrkpl) + .field("valA", &Slvs_Constraint::valA) + .field("ptA", &Slvs_Constraint::ptA) + .field("ptB", &Slvs_Constraint::ptB) + .field("entityA", &Slvs_Constraint::entityA) + .field("entityB", &Slvs_Constraint::entityB) + .field("entityC", &Slvs_Constraint::entityC) + .field("entityD", &Slvs_Constraint::entityD) + .field("other", &Slvs_Constraint::other) + .field("other2", &Slvs_Constraint::other2); + + emscripten::value_object("Slvs_SolveResult") + .field("result", &JsSolveResult::result) + .field("dof", &JsSolveResult::dof) + .field("nbad", &JsSolveResult::nbad) + .field("bad", &JsSolveResult::bad); + + emscripten::class_("Quaternion") + .constructor<>() + .function("plus", &Quaternion::Plus) + .function("minus", &Quaternion::Minus) + .function("scaledBy", &Quaternion::ScaledBy) + .function("magnitude", &Quaternion::Magnitude) + .function("withMagnitude", &Quaternion::WithMagnitude) + .function("toThe", &Quaternion::ToThe) + .function("inverse", &Quaternion::Inverse) + .function("times", &Quaternion::Times) + .function("mirror", &Quaternion::Mirror) + .function("rotationU", &Quaternion::RotationU) + .function("rotationV", &Quaternion::RotationV) + .function("rotationN", &Quaternion::RotationN) + .property("w", &Quaternion::w) + .property("vx", &Quaternion::vx) + .property("vy", &Quaternion::vy) + .property("vz", &Quaternion::vz) + .class_function("from", emscripten::select_overload(&Quaternion::From)); + + emscripten::class_("Vector") + .constructor<>() + .property("x", &Vector::x) + .property("y", &Vector::y) + .property("z", &Vector::z); + + emscripten::function("isFreeIn3D", &Slvs_IsFreeIn3D); + emscripten::function("is3D", &Slvs_Is3D); + emscripten::function("isNone", &Slvs_IsNone); + emscripten::function("isPoint2D", &Slvs_IsPoint2D); + emscripten::function("isPoint3D", &Slvs_IsPoint3D); + emscripten::function("isNormal2D", &Slvs_IsNormal2D); + emscripten::function("isNormal3D", &Slvs_IsNormal3D); + emscripten::function("isLine", &Slvs_IsLine); + emscripten::function("isLine2D", &Slvs_IsLine2D); + emscripten::function("isLine3D", &Slvs_IsLine3D); + emscripten::function("isCubic", &Slvs_IsCubic); + emscripten::function("isArc", &Slvs_IsArc); + emscripten::function("isWorkplane", &Slvs_IsWorkplane); + emscripten::function("isDistance", &Slvs_IsDistance); + emscripten::function("isPoint", &Slvs_IsPoint); + + emscripten::function("addPoint2D", &Slvs_AddPoint2D); + emscripten::function("addPoint3D", &Slvs_AddPoint3D); + emscripten::function("addNormal2D", &Slvs_AddNormal2D); + emscripten::function("addNormal3D", &Slvs_AddNormal3D); + emscripten::function("addDistance", &Slvs_AddDistance); + emscripten::function("addLine2D", &Slvs_AddLine2D); + emscripten::function("addLine3D", &Slvs_AddLine3D); + emscripten::function("addCubic", &Slvs_AddCubic); + emscripten::function("addArc", &Slvs_AddArc); + emscripten::function("addCircle", &Slvs_AddCircle); + emscripten::function("addWorkplane", &Slvs_AddWorkplane); + emscripten::function("addBase2D", &Slvs_AddBase2D); + + emscripten::function("addConstraint", &Slvs_AddConstraint); + emscripten::function("coincident", &Slvs_Coincident); + emscripten::function("distance", &Slvs_Distance); + emscripten::function("equal", &Slvs_Equal); + emscripten::function("equalAngle", &Slvs_EqualAngle); + emscripten::function("equalPointToLine", &Slvs_EqualPointToLine); + emscripten::function("ratio", &Slvs_Ratio); + emscripten::function("symmetric", &Slvs_Symmetric); + emscripten::function("symmetricH", &Slvs_SymmetricH); + emscripten::function("symmetricV", &Slvs_SymmetricV); + emscripten::function("midpoint", &Slvs_Midpoint); + emscripten::function("horizontal", &Slvs_Horizontal); + emscripten::function("vertical", &Slvs_Vertical); + emscripten::function("diameter", &Slvs_Diameter); + emscripten::function("sameOrientation", &Slvs_SameOrientation); + emscripten::function("angle", &Slvs_Angle); + emscripten::function("perpendicular", &Slvs_Perpendicular); + emscripten::function("parallel", &Slvs_Parallel); + emscripten::function("tangent", &Slvs_Tangent); + emscripten::function("distanceProj", &Slvs_DistanceProj); + emscripten::function("lengthDiff", &Slvs_LengthDiff); + emscripten::function("dragged", &Slvs_Dragged); + + emscripten::function("getParamValue", &Slvs_GetParamValue); + emscripten::function("setParamValue", &Slvs_SetParamValue); + emscripten::function("markDragged", &Slvs_MarkDragged); + emscripten::function("solveSketch", &solveSketch); + emscripten::function("clearSketch", &Slvs_ClearSketch); +} diff --git a/src/slvs/lib.cpp b/src/slvs/lib.cpp new file mode 100644 index 0000000..56c7c24 --- /dev/null +++ b/src/slvs/lib.cpp @@ -0,0 +1,1124 @@ +//----------------------------------------------------------------------------- +// A library wrapper around SolveSpace, to permit someone to use its constraint +// solver without coupling their program too much to SolveSpace's internals. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include +#include "solvespace.h" +#include +#include + +namespace SolveSpace { + +Sketch SK = {}; + +void Platform::FatalError(const std::string &message) { + fprintf(stderr, "%s", message.c_str()); + abort(); +} + +void Group::GenerateEquations(IdList *) { + // Nothing to do for now. +} + +} // namespace SolveSpace + +using namespace SolveSpace; + +static System SYS; +static ParamSet dragged; + +extern "C" { + +static ConstraintBase::Type Slvs_CTypeToConstraintBaseType(int type) { + switch(type) { +case SLVS_C_POINTS_COINCIDENT: return ConstraintBase::Type::POINTS_COINCIDENT; +case SLVS_C_PT_PT_DISTANCE: return ConstraintBase::Type::PT_PT_DISTANCE; +case SLVS_C_PT_PLANE_DISTANCE: return ConstraintBase::Type::PT_PLANE_DISTANCE; +case SLVS_C_PT_LINE_DISTANCE: return ConstraintBase::Type::PT_LINE_DISTANCE; +case SLVS_C_PT_FACE_DISTANCE: return ConstraintBase::Type::PT_FACE_DISTANCE; +case SLVS_C_PT_IN_PLANE: return ConstraintBase::Type::PT_IN_PLANE; +case SLVS_C_PT_ON_LINE: return ConstraintBase::Type::PT_ON_LINE; +case SLVS_C_PT_ON_FACE: return ConstraintBase::Type::PT_ON_FACE; +case SLVS_C_EQUAL_LENGTH_LINES: return ConstraintBase::Type::EQUAL_LENGTH_LINES; +case SLVS_C_LENGTH_RATIO: return ConstraintBase::Type::LENGTH_RATIO; +case SLVS_C_ARC_ARC_LEN_RATIO: return ConstraintBase::Type::ARC_ARC_LEN_RATIO; +case SLVS_C_ARC_LINE_LEN_RATIO: return ConstraintBase::Type::ARC_LINE_LEN_RATIO; +case SLVS_C_EQ_LEN_PT_LINE_D: return ConstraintBase::Type::EQ_LEN_PT_LINE_D; +case SLVS_C_EQ_PT_LN_DISTANCES: return ConstraintBase::Type::EQ_PT_LN_DISTANCES; +case SLVS_C_EQUAL_ANGLE: return ConstraintBase::Type::EQUAL_ANGLE; +case SLVS_C_EQUAL_LINE_ARC_LEN: return ConstraintBase::Type::EQUAL_LINE_ARC_LEN; +case SLVS_C_LENGTH_DIFFERENCE: return ConstraintBase::Type::LENGTH_DIFFERENCE; +case SLVS_C_ARC_ARC_DIFFERENCE: return ConstraintBase::Type::ARC_ARC_DIFFERENCE; +case SLVS_C_ARC_LINE_DIFFERENCE: return ConstraintBase::Type::ARC_LINE_DIFFERENCE; +case SLVS_C_SYMMETRIC: return ConstraintBase::Type::SYMMETRIC; +case SLVS_C_SYMMETRIC_HORIZ: return ConstraintBase::Type::SYMMETRIC_HORIZ; +case SLVS_C_SYMMETRIC_VERT: return ConstraintBase::Type::SYMMETRIC_VERT; +case SLVS_C_SYMMETRIC_LINE: return ConstraintBase::Type::SYMMETRIC_LINE; +case SLVS_C_AT_MIDPOINT: return ConstraintBase::Type::AT_MIDPOINT; +case SLVS_C_HORIZONTAL: return ConstraintBase::Type::HORIZONTAL; +case SLVS_C_VERTICAL: return ConstraintBase::Type::VERTICAL; +case SLVS_C_DIAMETER: return ConstraintBase::Type::DIAMETER; +case SLVS_C_PT_ON_CIRCLE: return ConstraintBase::Type::PT_ON_CIRCLE; +case SLVS_C_SAME_ORIENTATION: return ConstraintBase::Type::SAME_ORIENTATION; +case SLVS_C_ANGLE: return ConstraintBase::Type::ANGLE; +case SLVS_C_PARALLEL: return ConstraintBase::Type::PARALLEL; +case SLVS_C_PERPENDICULAR: return ConstraintBase::Type::PERPENDICULAR; +case SLVS_C_ARC_LINE_TANGENT: return ConstraintBase::Type::ARC_LINE_TANGENT; +case SLVS_C_CUBIC_LINE_TANGENT: return ConstraintBase::Type::CUBIC_LINE_TANGENT; +case SLVS_C_EQUAL_RADIUS: return ConstraintBase::Type::EQUAL_RADIUS; +case SLVS_C_PROJ_PT_DISTANCE: return ConstraintBase::Type::PROJ_PT_DISTANCE; +case SLVS_C_WHERE_DRAGGED: return ConstraintBase::Type::WHERE_DRAGGED; +case SLVS_C_CURVE_CURVE_TANGENT: return ConstraintBase::Type::CURVE_CURVE_TANGENT; +default: Platform::FatalError("bad constraint type " + std::to_string(type)); + } +} + +static EntityBase::Type Slvs_CTypeToEntityBaseType(int type) { + switch(type) { +case SLVS_E_POINT_IN_3D: return EntityBase::Type::POINT_IN_3D; +case SLVS_E_POINT_IN_2D: return EntityBase::Type::POINT_IN_2D; +case SLVS_E_NORMAL_IN_3D: return EntityBase::Type::NORMAL_IN_3D; +case SLVS_E_NORMAL_IN_2D: return EntityBase::Type::NORMAL_IN_2D; +case SLVS_E_DISTANCE: return EntityBase::Type::DISTANCE; +case SLVS_E_WORKPLANE: return EntityBase::Type::WORKPLANE; +case SLVS_E_LINE_SEGMENT: return EntityBase::Type::LINE_SEGMENT; +case SLVS_E_CUBIC: return EntityBase::Type::CUBIC; +case SLVS_E_CIRCLE: return EntityBase::Type::CIRCLE; +case SLVS_E_ARC_OF_CIRCLE: return EntityBase::Type::ARC_OF_CIRCLE; +default: Platform::FatalError("bad entity type " + std::to_string(type)); + } +} + +static bool Slvs_CanInitiallySatisfy(const ConstraintBase &c) { + switch(c.type) { + case ConstraintBase::Type::CUBIC_LINE_TANGENT: + case ConstraintBase::Type::PARALLEL: + // Can't initially satisfy if not projected onto a workplane + return c.workplane != EntityBase::FREE_IN_3D; + + case ConstraintBase::Type::PT_PT_DISTANCE: + case ConstraintBase::Type::PROJ_PT_DISTANCE: + case ConstraintBase::Type::PT_LINE_DISTANCE: + case ConstraintBase::Type::PT_PLANE_DISTANCE: + case ConstraintBase::Type::PT_FACE_DISTANCE: + case ConstraintBase::Type::EQUAL_LENGTH_LINES: + case ConstraintBase::Type::EQ_LEN_PT_LINE_D: + case ConstraintBase::Type::EQ_PT_LN_DISTANCES: + case ConstraintBase::Type::LENGTH_RATIO: + case ConstraintBase::Type::ARC_ARC_LEN_RATIO: + case ConstraintBase::Type::ARC_LINE_LEN_RATIO: + case ConstraintBase::Type::LENGTH_DIFFERENCE: + case ConstraintBase::Type::ARC_ARC_DIFFERENCE: + case ConstraintBase::Type::ARC_LINE_DIFFERENCE: + case ConstraintBase::Type::DIAMETER: + case ConstraintBase::Type::EQUAL_RADIUS: + case ConstraintBase::Type::EQUAL_LINE_ARC_LEN: + case ConstraintBase::Type::PT_IN_PLANE: + case ConstraintBase::Type::PT_ON_FACE: + case ConstraintBase::Type::PT_ON_CIRCLE: + case ConstraintBase::Type::HORIZONTAL: + case ConstraintBase::Type::VERTICAL: + case ConstraintBase::Type::PERPENDICULAR: + case ConstraintBase::Type::ANGLE: + case ConstraintBase::Type::EQUAL_ANGLE: + case ConstraintBase::Type::ARC_LINE_TANGENT: + case ConstraintBase::Type::CURVE_CURVE_TANGENT: + return true; + + case ConstraintBase::Type::AT_MIDPOINT: + // Can initially satisfy if between a line segment and a workplane + return c.ptA == EntityBase::NO_ENTITY; + + case ConstraintBase::Type::POINTS_COINCIDENT: + case ConstraintBase::Type::PT_ON_LINE: + case ConstraintBase::Type::SYMMETRIC: + case ConstraintBase::Type::SYMMETRIC_HORIZ: + case ConstraintBase::Type::SYMMETRIC_VERT: + case ConstraintBase::Type::SYMMETRIC_LINE: + case ConstraintBase::Type::SAME_ORIENTATION: + case ConstraintBase::Type::WHERE_DRAGGED: + case ConstraintBase::Type::COMMENT: + // Can't initially satisfy these constraints + return false; + } + ssassert(false, "Unexpected constraint type"); +} + +bool Slvs_IsFreeIn3D(Slvs_Entity e) { + return e.h == SLVS_FREE_IN_3D; +} + +bool Slvs_Is3D(Slvs_Entity e) { + return e.wrkpl == SLVS_FREE_IN_3D; +} + +bool Slvs_IsNone(Slvs_Entity e) { + return e.h == 0; +} + +bool Slvs_IsPoint2D(Slvs_Entity e) { + return e.type == SLVS_E_POINT_IN_2D; +} + +bool Slvs_IsPoint3D(Slvs_Entity e) { + return e.type == SLVS_E_POINT_IN_3D; +} + +bool Slvs_IsNormal2D(Slvs_Entity e) { + return e.type == SLVS_E_NORMAL_IN_2D; +} + +bool Slvs_IsNormal3D(Slvs_Entity e) { + return e.type == SLVS_E_NORMAL_IN_3D; +} + +bool Slvs_IsLine(Slvs_Entity e) { + return e.type == SLVS_E_LINE_SEGMENT; +} + +bool Slvs_IsLine2D(Slvs_Entity e) { + return e.type == SLVS_E_LINE_SEGMENT && !Slvs_Is3D(e); +} + +bool Slvs_IsLine3D(Slvs_Entity e) { + return e.type == SLVS_E_LINE_SEGMENT && Slvs_Is3D(e); +} + +bool Slvs_IsCubic(Slvs_Entity e) { + return e.type == SLVS_E_CUBIC; +} + +bool Slvs_IsArc(Slvs_Entity e) { + return e.type == SLVS_E_ARC_OF_CIRCLE; +} + +bool Slvs_IsWorkplane(Slvs_Entity e) { + return e.type == SLVS_E_WORKPLANE; +} + +bool Slvs_IsDistance(Slvs_Entity e) { + return e.type == SLVS_E_DISTANCE; +} + +bool Slvs_IsPoint(Slvs_Entity e) { + switch(e.type) { + case SLVS_E_POINT_IN_3D: + case SLVS_E_POINT_IN_2D: + return true; + default: + return false; + } +} + +bool Slvs_IsCircle(Slvs_Entity e) { + return e.type == SLVS_E_CIRCLE || e.type == SLVS_E_ARC_OF_CIRCLE; +} + +Slvs_hParam Slvs_AddParam(double val) { + Param pa = {}; + pa.val = val; + SK.param.AddAndAssignId(&pa); + return pa.h.v; +} + +// entities +Slvs_Entity Slvs_AddPoint2D(uint32_t grouph, double u, double v, Slvs_Entity workplane) { + Slvs_hParam uph = Slvs_AddParam(u); + Slvs_hParam vph = Slvs_AddParam(v); + EntityBase e = {}; + e.type = EntityBase::Type::POINT_IN_2D; + e.group.v = grouph; + e.workplane.v = workplane.h; + e.param[0].v = uph; + e.param[1].v = vph; + SK.entity.AddAndAssignId(&e); + + Slvs_Entity ce = Slvs_Entity {}; + ce.h = e.h.v; + ce.type = SLVS_E_POINT_IN_2D; + ce.group = grouph; + ce.wrkpl = workplane.h; + ce.param[0] = uph; + ce.param[1] = vph; + return ce; +} + +Slvs_Entity Slvs_AddPoint3D(uint32_t grouph, double x, double y, double z) { + Slvs_hParam xph = Slvs_AddParam(x); + Slvs_hParam yph = Slvs_AddParam(y); + Slvs_hParam zph = Slvs_AddParam(z); + EntityBase e = {}; + e.type = EntityBase::Type::POINT_IN_3D; + e.group.v = grouph; + e.workplane.v = EntityBase::FREE_IN_3D.v; + e.param[0].v = xph; + e.param[1].v = yph; + e.param[2].v = zph; + SK.entity.AddAndAssignId(&e); + + Slvs_Entity ce = Slvs_Entity {}; + ce.h = e.h.v; + ce.type = SLVS_E_POINT_IN_3D; + ce.group = grouph; + ce.wrkpl = SLVS_FREE_IN_3D; + ce.param[0] = xph; + ce.param[1] = yph; + ce.param[2] = zph; + return ce; +} + +Slvs_Entity Slvs_AddNormal2D(uint32_t grouph, Slvs_Entity workplane) { + if(!Slvs_IsWorkplane(workplane)) { + Platform::FatalError("workplane argument is not a workplane"); + } + EntityBase e = {}; + e.type = EntityBase::Type::NORMAL_IN_2D; + e.group.v = grouph; + e.workplane.v = workplane.h; + SK.entity.AddAndAssignId(&e); + + Slvs_Entity ce = Slvs_Entity {}; + ce.h = e.h.v; + ce.type = SLVS_E_NORMAL_IN_2D; + ce.group = grouph; + ce.wrkpl = workplane.h; + return ce; +} + +Slvs_Entity Slvs_AddNormal3D(uint32_t grouph, double qw, double qx, double qy, double qz) { + Slvs_hParam wph = Slvs_AddParam(qw); + Slvs_hParam xph = Slvs_AddParam(qx); + Slvs_hParam yph = Slvs_AddParam(qy); + Slvs_hParam zph = Slvs_AddParam(qz); + EntityBase e = {}; + e.type = EntityBase::Type::NORMAL_IN_3D; + e.group.v = grouph; + e.workplane.v = EntityBase::FREE_IN_3D.v; + e.param[0].v = wph; + e.param[1].v = xph; + e.param[2].v = yph; + e.param[3].v = zph; + SK.entity.AddAndAssignId(&e); + + Slvs_Entity ce = Slvs_Entity {}; + ce.h = e.h.v; + ce.type = SLVS_E_NORMAL_IN_3D; + ce.group = grouph; + ce.wrkpl = SLVS_FREE_IN_3D; + ce.param[0] = wph; + ce.param[1] = xph; + ce.param[2] = yph; + ce.param[3] = zph; + return ce; +} + +Slvs_Entity Slvs_AddDistance(uint32_t grouph, double value, Slvs_Entity workplane) { + if(!Slvs_IsWorkplane(workplane)) { + Platform::FatalError("workplane argument is not a workplane"); + } + Slvs_hParam valueph = Slvs_AddParam(value); + EntityBase e = {}; + e.type = EntityBase::Type::DISTANCE; + e.group.v = grouph; + e.workplane.v = workplane.h; + e.param[0].v = valueph; + SK.entity.AddAndAssignId(&e); + + Slvs_Entity ce = Slvs_Entity {}; + ce.h = e.h.v; + ce.type = SLVS_E_DISTANCE; + ce.group = grouph; + ce.wrkpl = workplane.h; + ce.param[0] = valueph; + return ce; +} + +Slvs_Entity Slvs_AddLine2D(uint32_t grouph, Slvs_Entity ptA, Slvs_Entity ptB, Slvs_Entity workplane) { + if(!Slvs_IsWorkplane(workplane)) { + Platform::FatalError("workplane argument is not a workplane"); + } else if(!Slvs_IsPoint2D(ptA)) { + Platform::FatalError("ptA argument is not a 2d point"); + } else if(!Slvs_IsPoint2D(ptB)) { + Platform::FatalError("ptB argument is not a 2d point"); + } + EntityBase e = {}; + e.type = EntityBase::Type::LINE_SEGMENT; + e.group.v = grouph; + e.workplane.v = workplane.h; + e.point[0].v = ptA.h; + e.point[1].v = ptB.h; + SK.entity.AddAndAssignId(&e); + + Slvs_Entity ce = Slvs_Entity {}; + ce.h = e.h.v; + ce.type = SLVS_E_LINE_SEGMENT; + ce.group = grouph; + ce.wrkpl = workplane.h; + ce.point[0] = ptA.h; + ce.point[1] = ptB.h; + return ce; +} + +Slvs_Entity Slvs_AddLine3D(uint32_t grouph, Slvs_Entity ptA, Slvs_Entity ptB) { + if(!Slvs_IsPoint3D(ptA)) { + Platform::FatalError("ptA argument is not a 3d point"); + } else if(!Slvs_IsPoint3D(ptB)) { + Platform::FatalError("ptB argument is not a 3d point"); + } + EntityBase e = {}; + e.type = EntityBase::Type::LINE_SEGMENT; + e.group.v = grouph; + e.workplane.v = EntityBase::FREE_IN_3D.v; + e.point[0].v = ptA.h; + e.point[1].v = ptB.h; + SK.entity.AddAndAssignId(&e); + + Slvs_Entity ce = Slvs_Entity {}; + ce.h = e.h.v; + ce.type = SLVS_E_LINE_SEGMENT; + ce.group = grouph; + ce.wrkpl = SLVS_FREE_IN_3D; + ce.point[0] = ptA.h; + ce.point[1] = ptB.h; + return ce; +} + +Slvs_Entity Slvs_AddCubic(uint32_t grouph, Slvs_Entity ptA, Slvs_Entity ptB, Slvs_Entity ptC, Slvs_Entity ptD, Slvs_Entity workplane) { + if(!Slvs_IsWorkplane(workplane)) { + Platform::FatalError("workplane argument is not a workplane"); + } else if(!Slvs_IsPoint2D(ptA)) { + Platform::FatalError("ptA argument is not a 2d point"); + } else if(!Slvs_IsPoint2D(ptB)) { + Platform::FatalError("ptB argument is not a 2d point"); + } else if(!Slvs_IsPoint2D(ptC)) { + Platform::FatalError("ptC argument is not a 2d point"); + } else if(!Slvs_IsPoint2D(ptD)) { + Platform::FatalError("ptD argument is not a 2d point"); + } + EntityBase e = {}; + e.type = EntityBase::Type::CUBIC; + e.group.v = grouph; + e.workplane.v = workplane.h; + e.point[0].v = ptA.h; + e.point[1].v = ptB.h; + e.point[2].v = ptC.h; + e.point[3].v = ptD.h; + SK.entity.AddAndAssignId(&e); + + Slvs_Entity ce = Slvs_Entity {}; + ce.h = e.h.v; + ce.type = SLVS_E_CUBIC; + ce.group = grouph; + ce.wrkpl = workplane.h; + ce.point[0] = ptA.h; + ce.point[1] = ptB.h; + ce.point[2] = ptC.h; + ce.point[3] = ptD.h; + return ce; +} + + +Slvs_Entity Slvs_AddArc(uint32_t grouph, Slvs_Entity normal, Slvs_Entity center, Slvs_Entity start, Slvs_Entity end, + Slvs_Entity workplane) { + if(!Slvs_IsWorkplane(workplane)) { + Platform::FatalError("workplane argument is not a workplane"); + } else if(!Slvs_IsNormal3D(normal)) { + Platform::FatalError("normal argument is not a 3d normal"); + } else if(!Slvs_IsPoint2D(center)) { + Platform::FatalError("center argument is not a 2d point"); + } else if(!Slvs_IsPoint2D(start)) { + Platform::FatalError("start argument is not a 2d point"); + } else if(!Slvs_IsPoint2D(end)) { + Platform::FatalError("end argument is not a 2d point"); + } + EntityBase e = {}; + e.type = EntityBase::Type::ARC_OF_CIRCLE; + e.group.v = grouph; + e.workplane.v = workplane.h; + e.normal.v = normal.h; + e.point[0].v = center.h; + e.point[1].v = start.h; + e.point[2].v = end.h; + SK.entity.AddAndAssignId(&e); + + Slvs_Entity ce = Slvs_Entity {}; + ce.h = e.h.v; + ce.type = SLVS_E_ARC_OF_CIRCLE; + ce.group = grouph; + ce.wrkpl = workplane.h; + ce.normal = normal.h; + ce.point[0] = center.h; + ce.point[1] = start.h; + ce.point[2] = end.h; + return ce; +} + +Slvs_Entity Slvs_AddCircle(uint32_t grouph, Slvs_Entity normal, Slvs_Entity center, Slvs_Entity radius, + Slvs_Entity workplane) { + if(!Slvs_IsWorkplane(workplane)) { + Platform::FatalError("workplane argument is not a workplane"); + } else if(!Slvs_IsNormal3D(normal)) { + Platform::FatalError("normal argument is not a 3d normal"); + } else if(!Slvs_IsPoint2D(center)) { + Platform::FatalError("center argument is not a 2d point"); + } else if(!Slvs_IsDistance(radius)) { + Platform::FatalError("radius argument is not a distance"); + } + EntityBase e = {}; + e.type = EntityBase::Type::CIRCLE; + e.group.v = grouph; + e.workplane.v = workplane.h; + e.normal.v = normal.h; + e.point[0].v = center.h; + e.distance.v = radius.h; + SK.entity.AddAndAssignId(&e); + + Slvs_Entity ce = Slvs_Entity {}; + ce.h = e.h.v; + ce.type = SLVS_E_CIRCLE; + ce.group = grouph; + ce.wrkpl = workplane.h; + ce.normal = normal.h; + ce.point[0] = center.h; + ce.distance = radius.h; + return ce; +} + +Slvs_Entity Slvs_AddWorkplane(uint32_t grouph, Slvs_Entity origin, Slvs_Entity nm) { + EntityBase e = {}; + e.type = EntityBase::Type::WORKPLANE; + e.group.v = grouph; + e.workplane.v = SLVS_FREE_IN_3D; + e.point[0].v = origin.h; + e.normal.v = nm.h; + SK.entity.AddAndAssignId(&e); + + Slvs_Entity ce = Slvs_Entity {}; + ce.h = e.h.v; + ce.type = SLVS_E_WORKPLANE; + ce.group = grouph; + ce.wrkpl = SLVS_FREE_IN_3D; + ce.point[0] = origin.h; + ce.normal = nm.h; + return ce; +} + +Slvs_Entity Slvs_AddBase2D(uint32_t grouph) { + Vector u = Vector::From(1, 0, 0); + Vector v = Vector::From(0, 1, 0); + Quaternion q = Quaternion::From(u, v); + Slvs_Entity nm = Slvs_AddNormal3D(grouph, q.w, q.vx, q.vy, q.vz); + return Slvs_AddWorkplane(grouph, Slvs_AddPoint3D(grouph, 0, 0, 0), nm); +} + +// constraints + +Slvs_Constraint Slvs_AddConstraint(uint32_t grouph, + int type, Slvs_Entity workplane, double val, Slvs_Entity ptA, + Slvs_Entity ptB = SLVS_E_NONE, Slvs_Entity entityA = SLVS_E_NONE, + Slvs_Entity entityB = SLVS_E_NONE, Slvs_Entity entityC = SLVS_E_NONE, + Slvs_Entity entityD = SLVS_E_NONE, int other = 0, int other2 = 0) { + ConstraintBase c = {}; + c.type = Slvs_CTypeToConstraintBaseType(type); + c.group.v = grouph; + c.workplane.v = workplane.h; + c.valA = val; + c.ptA.v = ptA.h; + c.ptB.v = ptB.h; + c.entityA.v = entityA.h; + c.entityB.v = entityB.h; + c.entityC.v = entityC.h; + c.entityD.v = entityD.h; + c.other = other ? true : false; + c.other2 = other2 ? true : false; + SK.constraint.AddAndAssignId(&c); + + Slvs_Constraint cc = Slvs_Constraint {}; + cc.h = c.h.v; + cc.type = type; + cc.group = grouph; + cc.wrkpl = workplane.h; + cc.valA = val; + cc.ptA = ptA.h; + cc.ptB = ptB.h; + cc.entityA = entityA.h; + cc.entityB = entityB.h; + cc.entityC = entityC.h; + cc.entityD = entityD.h; + cc.other = other ? true : false; + cc.other2 = other2 ? true : false; + return cc; +} + +Slvs_Constraint Slvs_Coincident(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, Slvs_Entity workplane = SLVS_E_FREE_IN_3D) { + if(Slvs_IsPoint(entityA) && Slvs_IsPoint(entityB)) { + return Slvs_AddConstraint(grouph, SLVS_C_POINTS_COINCIDENT, workplane, 0., entityA, entityB); + } else if(Slvs_IsPoint(entityA) && Slvs_IsWorkplane(entityB)) { + return Slvs_AddConstraint(grouph, SLVS_C_PT_IN_PLANE, SLVS_E_FREE_IN_3D, 0., entityA, SLVS_E_NONE, entityB); + } else if(Slvs_IsPoint(entityA) && Slvs_IsLine(entityB)) { + return Slvs_AddConstraint(grouph, SLVS_C_PT_ON_LINE, workplane, 0., entityA, SLVS_E_NONE, entityB); + } else if(Slvs_IsPoint(entityA) && (Slvs_IsCircle(entityB) || Slvs_IsArc(entityB))) { + return Slvs_AddConstraint(grouph, SLVS_C_PT_ON_CIRCLE, workplane, 0., entityA, SLVS_E_NONE, entityB); + } + Platform::FatalError("Invalid arguments for coincident constraint"); +} + +Slvs_Constraint Slvs_Distance(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, double value, Slvs_Entity workplane) { + if(Slvs_IsPoint(entityA) && Slvs_IsPoint(entityB)) { + return Slvs_AddConstraint(grouph, SLVS_C_PT_PT_DISTANCE, workplane, value, entityA, entityB); + } else if(Slvs_IsPoint(entityA) && Slvs_IsWorkplane(entityB) && Slvs_Is3D(workplane)) { + return Slvs_AddConstraint(grouph, SLVS_C_PT_PLANE_DISTANCE, entityB, value, entityA, SLVS_E_NONE, entityB); + } else if(Slvs_IsPoint(entityA) && Slvs_IsLine(entityB)) { + return Slvs_AddConstraint(grouph, SLVS_C_PT_LINE_DISTANCE, workplane, value, entityA, SLVS_E_NONE, entityB); + } + Platform::FatalError("Invalid arguments for distance constraint"); +} + +Slvs_Constraint Slvs_Equal(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, Slvs_Entity workplane = SLVS_E_FREE_IN_3D) { + if(Slvs_IsLine(entityA) && Slvs_IsLine(entityB)) { + return Slvs_AddConstraint(grouph, SLVS_C_EQUAL_LENGTH_LINES, workplane, 0., SLVS_E_NONE, SLVS_E_NONE, entityA, entityB); + } else if(Slvs_IsLine(entityA) && (Slvs_IsArc(entityB) || Slvs_IsCircle(entityB))) { + return Slvs_AddConstraint(grouph, SLVS_C_EQUAL_LINE_ARC_LEN, workplane, 0., SLVS_E_NONE, SLVS_E_NONE, entityA, entityB); + } else if((Slvs_IsArc(entityA) || Slvs_IsCircle(entityA)) && (Slvs_IsArc(entityB) || Slvs_IsCircle(entityB))) { + return Slvs_AddConstraint(grouph, SLVS_C_EQUAL_RADIUS, workplane, 0., SLVS_E_NONE, SLVS_E_NONE, entityA, entityB); + } + Platform::FatalError("Invalid arguments for equal constraint"); +} + +Slvs_Constraint Slvs_EqualAngle(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, Slvs_Entity entityC, Slvs_Entity entityD, Slvs_Entity workplane = SLVS_E_FREE_IN_3D) { + if(Slvs_IsLine2D(entityA) && Slvs_IsLine2D(entityB) && Slvs_IsLine2D(entityC) && Slvs_IsLine2D(entityD) && (Slvs_IsWorkplane(workplane) || Slvs_IsFreeIn3D(workplane))) { + return Slvs_AddConstraint(grouph, SLVS_C_EQUAL_ANGLE, workplane, 0., SLVS_E_NONE, SLVS_E_NONE, entityA, entityB, entityC, entityD); + } + Platform::FatalError("Invalid arguments for equal angle constraint"); +} + +Slvs_Constraint Slvs_EqualPointToLine(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, Slvs_Entity entityC, Slvs_Entity entityD, Slvs_Entity workplane = SLVS_E_FREE_IN_3D) { + if(Slvs_IsPoint2D(entityA) && Slvs_IsLine2D(entityB) && Slvs_IsPoint2D(entityC) && Slvs_IsLine2D(entityD) && (Slvs_IsWorkplane(workplane) || Slvs_IsFreeIn3D(workplane))) { + return Slvs_AddConstraint(grouph, SLVS_C_EQ_PT_LN_DISTANCES, workplane, 0., entityA, entityB, entityC, entityD); + } + Platform::FatalError("Invalid arguments for equal point to line constraint"); +} + +Slvs_Constraint Slvs_Ratio(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, double value, Slvs_Entity workplane = SLVS_E_FREE_IN_3D) { + if(Slvs_IsLine2D(entityA) && Slvs_IsLine2D(entityB) && (Slvs_IsWorkplane(workplane) || Slvs_IsFreeIn3D(workplane))) { + return Slvs_AddConstraint(grouph, SLVS_C_LENGTH_RATIO, workplane, value, SLVS_E_NONE, SLVS_E_NONE, entityA, entityB); + } + Platform::FatalError("Invalid arguments for ratio constraint"); +} + +Slvs_Constraint Slvs_Symmetric(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, Slvs_Entity entityC = SLVS_E_NONE, Slvs_Entity workplane = SLVS_E_FREE_IN_3D) { + if(Slvs_IsPoint3D(entityA) && Slvs_IsPoint3D(entityB) && Slvs_IsWorkplane(entityC) && Slvs_IsFreeIn3D(workplane)) { + return Slvs_AddConstraint(grouph, SLVS_C_SYMMETRIC, workplane, 0., entityA, entityB, entityC); + } else if(Slvs_IsPoint2D(entityA) && Slvs_IsPoint2D(entityB) && Slvs_IsWorkplane(entityC) && Slvs_IsFreeIn3D(workplane)) { + return Slvs_AddConstraint(grouph, SLVS_C_SYMMETRIC, entityC, 0., entityA, entityB, entityC); + } else if(Slvs_IsPoint2D(entityA) && Slvs_IsPoint2D(entityB) && Slvs_IsLine(entityC)) { + if(Slvs_IsFreeIn3D(workplane)) { + Platform::FatalError("3d workplane given for a 2d constraint"); + } + return Slvs_AddConstraint(grouph, SLVS_C_SYMMETRIC_LINE, workplane, 0., entityA, entityB, entityC); + } + Platform::FatalError("Invalid arguments for symmetric constraint"); +} + +Slvs_Constraint Slvs_SymmetricH(uint32_t grouph, Slvs_Entity ptA, Slvs_Entity ptB, Slvs_Entity workplane) { + if(Slvs_IsFreeIn3D(workplane)) { + Platform::FatalError("3d workplane given for a 2d constraint"); + } else if(Slvs_IsPoint2D(ptA) && Slvs_IsPoint2D(ptB)) { + return Slvs_AddConstraint(grouph, SLVS_C_SYMMETRIC_HORIZ, workplane, 0., ptA, ptB); + } + Platform::FatalError("Invalid arguments for symmetric horizontal constraint"); +} + +Slvs_Constraint Slvs_SymmetricV(uint32_t grouph, Slvs_Entity ptA, Slvs_Entity ptB, Slvs_Entity workplane) { + if(Slvs_IsFreeIn3D(workplane)) { + Platform::FatalError("3d workplane given for a 2d constraint"); + } else if(Slvs_IsPoint2D(ptA) && Slvs_IsPoint2D(ptB)) { + return Slvs_AddConstraint(grouph, SLVS_C_SYMMETRIC_VERT, workplane, 0., ptA, ptB); + } + Platform::FatalError("Invalid arguments for symmetric vertical constraint"); +} + +Slvs_Constraint Slvs_Midpoint(uint32_t grouph, Slvs_Entity ptA, Slvs_Entity ptB, Slvs_Entity workplane = SLVS_E_FREE_IN_3D) { + if(Slvs_IsPoint(ptA) && Slvs_IsLine(ptB) && (Slvs_IsWorkplane(workplane) || Slvs_IsFreeIn3D(workplane))) { + return Slvs_AddConstraint(grouph, SLVS_C_AT_MIDPOINT, workplane, 0., ptA, SLVS_E_NONE, ptB); + } + Platform::FatalError("Invalid arguments for midpoint constraint"); +} + +Slvs_Constraint Slvs_Horizontal(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity workplane, Slvs_Entity entityB = SLVS_E_NONE) { + if(Slvs_IsFreeIn3D(workplane)) { + Platform::FatalError("Horizontal constraint is not supported in 3D"); + } else if(Slvs_IsLine2D(entityA)) { + return Slvs_AddConstraint(grouph, SLVS_C_HORIZONTAL, workplane, 0., SLVS_E_NONE, SLVS_E_NONE, entityA); + } else if(Slvs_IsPoint2D(entityA) && Slvs_IsPoint2D(entityB)) { + return Slvs_AddConstraint(grouph, SLVS_C_HORIZONTAL, workplane, 0., entityA, entityB); + } + Platform::FatalError("Invalid arguments for horizontal constraint"); +} + +Slvs_Constraint Slvs_Vertical(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity workplane, Slvs_Entity entityB = SLVS_E_NONE) { + if(Slvs_IsFreeIn3D(workplane)) { + Platform::FatalError("Vertical constraint is not supported in 3D"); + } else if(Slvs_IsLine2D(entityA)) { + return Slvs_AddConstraint(grouph, SLVS_C_VERTICAL, workplane, 0., SLVS_E_NONE, SLVS_E_NONE, entityA); + } else if(Slvs_IsPoint2D(entityA) && Slvs_IsPoint2D(entityB)) { + return Slvs_AddConstraint(grouph, SLVS_C_VERTICAL, workplane, 0., entityA, entityB); + } + Platform::FatalError("Invalid arguments for horizontal constraint"); +} + +Slvs_Constraint Slvs_Diameter(uint32_t grouph, Slvs_Entity entityA, double value) { + if(Slvs_IsArc(entityA) || Slvs_IsCircle(entityA)) { + return Slvs_AddConstraint(grouph, SLVS_C_DIAMETER, SLVS_E_FREE_IN_3D, value, SLVS_E_NONE, SLVS_E_NONE, entityA); + } + Platform::FatalError("Invalid arguments for diameter constraint"); +} + +Slvs_Constraint Slvs_SameOrientation(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB) { + if(Slvs_IsNormal3D(entityA) && Slvs_IsNormal3D(entityB)) { + return Slvs_AddConstraint(grouph, SLVS_C_SAME_ORIENTATION, SLVS_E_FREE_IN_3D, 0., SLVS_E_NONE, SLVS_E_NONE, entityA, entityB); + } + Platform::FatalError("Invalid arguments for same orientation constraint"); +} + +Slvs_Constraint Slvs_Angle(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, double value, Slvs_Entity workplane = SLVS_E_FREE_IN_3D, int inverse = 0) { + if(Slvs_IsLine2D(entityA) && Slvs_IsLine2D(entityB) && (Slvs_IsWorkplane(workplane) || Slvs_IsFreeIn3D(workplane))) { + return Slvs_AddConstraint(grouph, SLVS_C_ANGLE, workplane, value, SLVS_E_NONE, SLVS_E_NONE, entityA, entityB, SLVS_E_NONE, SLVS_E_NONE, inverse); + } + Platform::FatalError("Invalid arguments for angle constraint"); +} + +Slvs_Constraint Slvs_Perpendicular(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, Slvs_Entity workplane = SLVS_E_FREE_IN_3D, int inverse = 0) { + if(Slvs_IsLine2D(entityA) && Slvs_IsLine2D(entityB) && (Slvs_IsWorkplane(workplane) || Slvs_IsFreeIn3D(workplane))) { + return Slvs_AddConstraint(grouph, SLVS_C_PERPENDICULAR, workplane, 0., SLVS_E_NONE, SLVS_E_NONE, entityA, entityB, SLVS_E_NONE, SLVS_E_NONE, inverse); + } + Platform::FatalError("Invalid arguments for perpendicular constraint"); +} + +Slvs_Constraint Slvs_Parallel(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, Slvs_Entity workplane = SLVS_E_FREE_IN_3D) { + if(Slvs_IsLine2D(entityA) && Slvs_IsLine2D(entityB) && (Slvs_IsWorkplane(workplane) || Slvs_IsFreeIn3D(workplane))) { + return Slvs_AddConstraint(grouph, SLVS_C_PARALLEL, workplane, 0., SLVS_E_NONE, SLVS_E_NONE, entityA, entityB); + } + Platform::FatalError("Invalid arguments for parallel constraint"); +} + +Slvs_Constraint Slvs_Tangent(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, Slvs_Entity workplane = SLVS_E_FREE_IN_3D) { + if(Slvs_IsArc(entityA) && Slvs_IsLine2D(entityB)) { + if(Slvs_IsFreeIn3D(workplane)) { + Platform::FatalError("3d workplane given for a 2d constraint"); + } + Vector a1 = SK.entity.FindById(hEntity { entityA.point[1] })->PointGetNum(), + a2 = SK.entity.FindById(hEntity { entityA.point[2] })->PointGetNum(); + Vector l0 = SK.entity.FindById(hEntity { entityB.point[0] })->PointGetNum(), + l1 = SK.entity.FindById(hEntity { entityB.point[1] })->PointGetNum(); + int other; + if(l0.Equals(a1) || l1.Equals(a1)) { + other = 0; + } else if(l0.Equals(a2) || l1.Equals(a2)) { + other = 1; + } else { + Platform::FatalError("The tangent arc and line segment must share an " + "endpoint. Constrain them with Constrain -> " + "On Point before constraining tangent."); + } + return Slvs_AddConstraint(grouph, SLVS_C_ARC_LINE_TANGENT, workplane, 0., SLVS_E_NONE, SLVS_E_NONE, entityA, entityB, SLVS_E_NONE, SLVS_E_NONE, other); + } else if(Slvs_IsCubic(entityA) && Slvs_IsLine2D(entityB) && Slvs_IsFreeIn3D(workplane)) { + EntityBase* skEntityA = SK.entity.FindById(hEntity { entityA.h }); + Vector as = skEntityA->CubicGetStartNum(), af = skEntityA->CubicGetFinishNum(); + Vector l0 = SK.entity.FindById(hEntity { entityB.point[0] })->PointGetNum(), + l1 = SK.entity.FindById(hEntity { entityB.point[1] })->PointGetNum(); + int other; + if(l0.Equals(as) || l1.Equals(as)) { + other = 0; + } else if(l0.Equals(af) || l1.Equals(af)) { + other = 1; + } else { + Platform::FatalError("The tangent cubic and line segment must share an " + "endpoint. Constrain them with Constrain -> " + "On Point before constraining tangent."); + } + return Slvs_AddConstraint(grouph, SLVS_C_CUBIC_LINE_TANGENT, workplane, 0., SLVS_E_NONE, SLVS_E_NONE, entityA, entityB, SLVS_E_NONE, SLVS_E_NONE, other); + } else if((Slvs_IsArc(entityA) || Slvs_IsCubic(entityA)) && (Slvs_IsArc(entityB) || Slvs_IsCubic(entityB))) { + if(Slvs_IsFreeIn3D(workplane)) { + Platform::FatalError("3d workplane given for a 2d constraint"); + } + EntityBase* skEntityA = SK.entity.FindById(hEntity { entityA.h }); + EntityBase* skEntityB = SK.entity.FindById(hEntity { entityB.h }); + Vector as = skEntityA->EndpointStart(), af = skEntityA->EndpointFinish(), + bs = skEntityB->EndpointStart(), bf = skEntityB->EndpointFinish(); + int other; + int other2; + if(as.Equals(bs)) { + other = 0; + other2 = 0; + } else if(as.Equals(bf)) { + other = 0; + other2 = 1; + } else if(af.Equals(bs)) { + other = 1; + other2 = 0; + } else if(af.Equals(bf)) { + other = 1; + other2 = 1; + } else { + Platform::FatalError("The curves must share an endpoint. Constrain them " + "with Constrain -> On Point before constraining " + "tangent."); + } + return Slvs_AddConstraint(grouph, SLVS_C_CURVE_CURVE_TANGENT, workplane, 0., SLVS_E_NONE, SLVS_E_NONE, entityA, entityB, SLVS_E_NONE, SLVS_E_NONE, other, other2); + } + Platform::FatalError("Invalid arguments for tangent constraint"); +} + +Slvs_Constraint Slvs_DistanceProj(uint32_t grouph, Slvs_Entity ptA, Slvs_Entity ptB, double value) { + if(Slvs_IsPoint(ptA) && Slvs_IsPoint(ptB)) { + return Slvs_AddConstraint(grouph, SLVS_C_PROJ_PT_DISTANCE, SLVS_E_FREE_IN_3D, value, ptA, ptB); + } + Platform::FatalError("Invalid arguments for projected distance constraint"); +} + +Slvs_Constraint Slvs_LengthDiff(uint32_t grouph, Slvs_Entity entityA, Slvs_Entity entityB, double value, Slvs_Entity workplane = SLVS_E_FREE_IN_3D) { + if(Slvs_IsLine(entityA) && Slvs_IsLine(entityB) && (Slvs_IsWorkplane(workplane) || Slvs_IsFreeIn3D(workplane))) { + return Slvs_AddConstraint(grouph, SLVS_C_LENGTH_DIFFERENCE, workplane, value, SLVS_E_NONE, SLVS_E_NONE, entityA, entityB); + } + Platform::FatalError("Invalid arguments for length difference constraint"); +} + +Slvs_Constraint Slvs_Dragged(uint32_t grouph, Slvs_Entity ptA, Slvs_Entity workplane = SLVS_E_FREE_IN_3D) { + if(Slvs_IsPoint(ptA) && (Slvs_IsWorkplane(workplane) || Slvs_IsFreeIn3D(workplane))) { + return Slvs_AddConstraint(grouph, SLVS_C_WHERE_DRAGGED, workplane, 0., ptA); + } + Platform::FatalError("Invalid arguments for dragged constraint"); +} + +void Slvs_QuaternionU(double qw, double qx, double qy, double qz, + double *x, double *y, double *z) +{ + Quaternion q = Quaternion::From(qw, qx, qy, qz); + Vector v = q.RotationU(); + *x = v.x; + *y = v.y; + *z = v.z; +} + +void Slvs_QuaternionV(double qw, double qx, double qy, double qz, + double *x, double *y, double *z) +{ + Quaternion q = Quaternion::From(qw, qx, qy, qz); + Vector v = q.RotationV(); + *x = v.x; + *y = v.y; + *z = v.z; +} + +void Slvs_QuaternionN(double qw, double qx, double qy, double qz, + double *x, double *y, double *z) +{ + Quaternion q = Quaternion::From(qw, qx, qy, qz); + Vector v = q.RotationN(); + *x = v.x; + *y = v.y; + *z = v.z; +} + +void Slvs_MakeQuaternion(double ux, double uy, double uz, + double vx, double vy, double vz, + double *qw, double *qx, double *qy, double *qz) +{ + Vector u = Vector::From(ux, uy, uz), + v = Vector::From(vx, vy, vz); + Quaternion q = Quaternion::From(u, v); + *qw = q.w; + *qx = q.vx; + *qy = q.vy; + *qz = q.vz; +} + +void Slvs_ClearSketch() +{ + dragged.clear(); + SYS.Clear(); + SK.param.Clear(); + SK.entity.Clear(); + SK.constraint.Clear(); +} + +void Slvs_MarkDragged(Slvs_Entity ptA) { + if(Slvs_IsPoint(ptA)) { + const size_t params = Slvs_IsPoint3D(ptA) ? 3 : 2; + for(size_t i = 0; i < params; ++i) { + hParam p = hParam { ptA.param[i] }; + dragged.insert(p); + } + } else { + SolveSpace::Platform::FatalError("Invalid entity for marking dragged"); + } +} + +Slvs_SolveResult Slvs_SolveSketch(uint32_t shg, Slvs_hConstraint **bad = nullptr) +{ + SYS.Clear(); + + Group g = {}; + g.h.v = shg; + + // add params from entities on sketch + for(EntityBase &ent : SK.entity) { + EntityBase *e = &ent; + // skip entities from other groups + if (e->group.v != shg) { + continue; + } + for (hParam &parh : e->param) { + if (parh.v != 0) { + // get params for this entity and add it to the system + Param *p = SK.GetParam(parh); + p->known = false; + SYS.param.Add(p); + } + } + } + + // add params from constraints + for(ConstraintBase &con : SK.constraint) { + ConstraintBase *c = &con; + if(c->group.v != shg) + continue; + // If we're solving a sketch twice without calling `Slvs_ClearSketch()` in between, + // we already have a constraint param in the sketch, and regeneration would simply + // create another one and orphan the existing one. While this doesn't create any + // correctness issues, it does waste memory, so identify this case and regenerate + // only if we actually need to. + if(c->valP.v) { + SYS.param.Add(SK.GetParam(c->valP)); + continue; + } + // If `valP` is 0, this is either a constraint which doesn't have a param, or one + // which we haven't seen before, so try to regenerate. + // This generates at most a single additional param + c->Generate(&SK.param); + if(c->valP.v) { + SYS.param.Add(SK.GetParam(c->valP)); + + if(Slvs_CanInitiallySatisfy(*c)) { + c->ModifyToSatisfy(); + } + } + } + + // mark dragged params + for(hParam p : dragged) { + SYS.dragged.insert(p); + } + + // for(hParam &par : SYS.dragged) { + // std::cout << "DraggedParam( h:" << par.v << " )\n"; + // } + + // for(Param &par : SYS.param) { + // std::cout << "SysParam( " << par.ToString() << " )\n"; + // } + + // for(EntityBase &ent : SK.entity) { + // std::cout << "SketchEntityBase( " << ent.ToString() << " )\n"; + // } + + // for(ConstraintBase &con : SK.constraint) { + // std::cout << "SketchConstraintBase( " << con.ToString() << " )\n"; + // } + + List badList; + bool andFindBad = bad != nullptr; + + int dof = 0; + SolveResult status = SYS.Solve(&g, &dof, &badList, andFindBad, false, false); + Slvs_SolveResult sr = {}; + sr.dof = dof; + sr.nbad = badList.n; + if(bad) { + if(sr.nbad <= 0) { + *bad = nullptr; + } else { + *bad = static_cast(malloc(sizeof(Slvs_hConstraint) * sr.nbad)); + for(int i = 0; i < sr.nbad; ++i) { + (*bad)[i] = badList[i].v; + } + } + } + sr.result = 0; + switch(status) { + case SolveResult::OKAY: { + sr.result = SLVS_RESULT_OKAY; + return sr; + } + case SolveResult::DIDNT_CONVERGE: { + sr.result = SLVS_RESULT_DIDNT_CONVERGE; + return sr; + } + case SolveResult::REDUNDANT_DIDNT_CONVERGE: { + sr.result = SLVS_RESULT_INCONSISTENT; + return sr; + } + case SolveResult::REDUNDANT_OKAY: { + sr.result = SLVS_RESULT_REDUNDANT_OKAY; + return sr; + } + case SolveResult::TOO_MANY_UNKNOWNS: { + sr.result = SLVS_RESULT_TOO_MANY_UNKNOWNS; + return sr; + } + } + return sr; +} + +double Slvs_GetParamValue(uint32_t ph) +{ + Param* p = SK.param.FindById(hParam { ph }); + return p->val; +} + +void Slvs_SetParamValue(uint32_t ph, double value) +{ + Param* p = SK.param.FindById(hParam { ph }); + p->val = value; +} + +void Slvs_Solve(Slvs_System *ssys, uint32_t shg) +{ + SYS.Clear(); + SK.param.Clear(); + SK.entity.Clear(); + SK.constraint.Clear(); + int i; + for(i = 0; i < ssys->params; i++) { + Slvs_Param *sp = &(ssys->param[i]); + Param p = {}; + + p.h.v = sp->h; + p.val = sp->val; + SK.param.Add(&p); + if(sp->group == shg) { + SYS.param.Add(&p); + } + } + + for(i = 0; i < ssys->entities; i++) { + Slvs_Entity *se = &(ssys->entity[i]); + EntityBase e = {}; + e.type = Slvs_CTypeToEntityBaseType(se->type); + e.h.v = se->h; + e.group.v = se->group; + e.workplane.v = se->wrkpl; + e.point[0].v = se->point[0]; + e.point[1].v = se->point[1]; + e.point[2].v = se->point[2]; + e.point[3].v = se->point[3]; + e.normal.v = se->normal; + e.distance.v = se->distance; + e.param[0].v = se->param[0]; + e.param[1].v = se->param[1]; + e.param[2].v = se->param[2]; + e.param[3].v = se->param[3]; + + SK.entity.Add(&e); + } + ParamList params = {}; + for(i = 0; i < ssys->constraints; i++) { + Slvs_Constraint *sc = &(ssys->constraint[i]); + ConstraintBase c = {}; + c.type = Slvs_CTypeToConstraintBaseType(sc->type); + c.h.v = sc->h; + c.group.v = sc->group; + c.workplane.v = sc->wrkpl; + c.valA = sc->valA; + c.ptA.v = sc->ptA; + c.ptB.v = sc->ptB; + c.entityA.v = sc->entityA; + c.entityB.v = sc->entityB; + c.entityC.v = sc->entityC; + c.entityD.v = sc->entityD; + c.other = (sc->other) ? true : false; + c.other2 = (sc->other2) ? true : false; + + c.Generate(¶ms); + if(!params.IsEmpty()) { + for(Param &p : params) { + p.h = SK.param.AddAndAssignId(&p); + c.valP = p.h; + SYS.param.Add(&p); + } + params.Clear(); + + if(Slvs_CanInitiallySatisfy(c)) { + c.ModifyToSatisfy(); + } + } + + SK.constraint.Add(&c); + } + + for(i = 0; i < ssys->ndragged; i++) { + if(ssys->dragged[i]) { + hParam hp = { ssys->dragged[i] }; + SYS.dragged.insert(hp); + } + } + + Group g = {}; + g.h.v = shg; + + List bad = {}; + + // Now we're finally ready to solve! + bool andFindBad = ssys->calculateFaileds ? true : false; + SolveResult how = SYS.Solve(&g, &(ssys->dof), &bad, andFindBad, /*andFindFree=*/false); + + switch(how) { + case SolveResult::OKAY: + ssys->result = SLVS_RESULT_OKAY; + break; + + case SolveResult::DIDNT_CONVERGE: + ssys->result = SLVS_RESULT_DIDNT_CONVERGE; + break; + + case SolveResult::REDUNDANT_DIDNT_CONVERGE: + ssys->result = SLVS_RESULT_INCONSISTENT; + break; + + case SolveResult::REDUNDANT_OKAY: + ssys->result = SLVS_RESULT_REDUNDANT_OKAY; + break; + + case SolveResult::TOO_MANY_UNKNOWNS: + ssys->result = SLVS_RESULT_TOO_MANY_UNKNOWNS; + break; + } + + // Write the new parameter values back to our caller. + for(i = 0; i < ssys->params; i++) { + Slvs_Param *sp = &(ssys->param[i]); + hParam hp = { sp->h }; + sp->val = SK.GetParam(hp)->val; + } + + if(ssys->failed) { + // Copy over any the list of problematic constraints. + for(i = 0; i < ssys->faileds && i < bad.n; i++) { + ssys->failed[i] = bad[i].v; + } + ssys->faileds = bad.n; + } + + bad.Clear(); + SYS.Clear(); + SK.param.Clear(); + SK.entity.Clear(); + SK.constraint.Clear(); + + Platform::FreeAllTemporary(); +} + +} /* extern "C" */ diff --git a/src/slvs/lib.pyx b/src/slvs/lib.pyx new file mode 100644 index 0000000..84e1b02 --- /dev/null +++ b/src/slvs/lib.pyx @@ -0,0 +1,391 @@ +#cython: language_level=3 +from enum import IntEnum, auto +from libc.stdint cimport uint32_t +from libc.stdlib cimport free + +cdef extern from "slvs.h" nogil: + ctypedef uint32_t Slvs_hEntity + ctypedef uint32_t Slvs_hGroup + ctypedef uint32_t Slvs_hConstraint + ctypedef uint32_t Slvs_hParam + + ctypedef struct Slvs_Entity: + Slvs_hEntity h + Slvs_hGroup group + int type + Slvs_hEntity wrkpl + Slvs_hEntity point[4] + Slvs_hEntity normal + Slvs_hEntity distance + Slvs_hParam param[4] + + ctypedef struct Slvs_Constraint: + Slvs_hConstraint h + Slvs_hGroup group + int type + Slvs_hEntity wrkpl + double valA + Slvs_hEntity ptA + Slvs_hEntity ptB + Slvs_hEntity entityA + Slvs_hEntity entityB + Slvs_hEntity entityC + Slvs_hEntity entityD + int other + int other2 + + ctypedef struct Slvs_SolveResult: + int result + int dof + int nbad + + void Slvs_QuaternionU(double qw, double qx, double qy, double qz, + double *x, double *y, double *z) + void Slvs_QuaternionV(double qw, double qx, double qy, double qz, + double *x, double *y, double *z) + void Slvs_QuaternionN(double qw, double qx, double qy, double qz, + double *x, double *y, double *z) + + void Slvs_MakeQuaternion(double ux, double uy, double uz, + double vx, double vy, double vz, + double *qw, double *qx, double *qy, double *qz) + + Slvs_Entity Slvs_AddPoint2D(Slvs_hGroup grouph, double u, double v, Slvs_Entity workplane) + Slvs_Entity Slvs_AddPoint3D(Slvs_hGroup grouph, double x, double y, double z) + Slvs_Entity Slvs_AddNormal2D(Slvs_hGroup grouph, Slvs_Entity workplane) + Slvs_Entity Slvs_AddNormal3D(Slvs_hGroup grouph, double qw, double qx, double qy, double qz) + Slvs_Entity Slvs_AddDistance(Slvs_hGroup grouph, double value, Slvs_Entity workplane) + Slvs_Entity Slvs_AddLine2D(Slvs_hGroup grouph, Slvs_Entity ptA, Slvs_Entity ptB, Slvs_Entity workplane) + Slvs_Entity Slvs_AddLine3D(Slvs_hGroup grouph, Slvs_Entity ptA, Slvs_Entity ptB) + Slvs_Entity Slvs_AddCubic(Slvs_hGroup grouph, Slvs_Entity ptA, Slvs_Entity ptB, Slvs_Entity ptC, Slvs_Entity ptD, Slvs_Entity workplane) + Slvs_Entity Slvs_AddArc(Slvs_hGroup grouph, Slvs_Entity normal, Slvs_Entity center, Slvs_Entity start, Slvs_Entity end, Slvs_Entity workplane) + Slvs_Entity Slvs_AddCircle(Slvs_hGroup grouph, Slvs_Entity normal, Slvs_Entity center, Slvs_Entity radius, Slvs_Entity workplane) + Slvs_Entity Slvs_AddWorkplane(Slvs_hGroup grouph, Slvs_Entity origin, Slvs_Entity nm) + Slvs_Entity Slvs_AddBase2D(Slvs_hGroup grouph) + + Slvs_Constraint Slvs_AddConstraint(Slvs_hGroup grouph, int type, Slvs_Entity workplane, double val, Slvs_Entity ptA, Slvs_Entity ptB, Slvs_Entity entityA, Slvs_Entity entityB, Slvs_Entity entityC, Slvs_Entity entityD, int other, int other2) + Slvs_Constraint Slvs_Coincident(Slvs_hGroup grouph, Slvs_Entity entityA, Slvs_Entity entityB, Slvs_Entity workplane) + Slvs_Constraint Slvs_Distance(Slvs_hGroup grouph, Slvs_Entity entityA, Slvs_Entity entityB, double value, Slvs_Entity workplane) + Slvs_Constraint Slvs_Equal(Slvs_hGroup grouph, Slvs_Entity entityA, Slvs_Entity entityB, Slvs_Entity workplane) + Slvs_Constraint Slvs_EqualAngle(Slvs_hGroup grouph, Slvs_Entity entityA, Slvs_Entity entityB, Slvs_Entity entityC, Slvs_Entity entityD, Slvs_Entity workplane) + Slvs_Constraint Slvs_EqualPointToLine(Slvs_hGroup grouph, Slvs_Entity entityA, Slvs_Entity entityB, Slvs_Entity entityC, Slvs_Entity entityD, Slvs_Entity workplane) + Slvs_Constraint Slvs_Ratio(Slvs_hGroup grouph, Slvs_Entity entityA, Slvs_Entity entityB, double value, Slvs_Entity workplane) + Slvs_Constraint Slvs_Symmetric(Slvs_hGroup grouph, Slvs_Entity entityA, Slvs_Entity entityB, Slvs_Entity entityC, Slvs_Entity workplane) + Slvs_Constraint Slvs_SymmetricH(Slvs_hGroup grouph, Slvs_Entity ptA, Slvs_Entity ptB, Slvs_Entity workplane) + Slvs_Constraint Slvs_SymmetricV(Slvs_hGroup grouph, Slvs_Entity ptA, Slvs_Entity ptB, Slvs_Entity workplane) + Slvs_Constraint Slvs_Midpoint(Slvs_hGroup grouph, Slvs_Entity ptA, Slvs_Entity ptB, Slvs_Entity workplane) + Slvs_Constraint Slvs_Horizontal(Slvs_hGroup grouph, Slvs_Entity entityA, Slvs_Entity workplane, Slvs_Entity entityB) + Slvs_Constraint Slvs_Vertical(Slvs_hGroup grouph, Slvs_Entity entityA, Slvs_Entity workplane, Slvs_Entity entityB) + Slvs_Constraint Slvs_Diameter(Slvs_hGroup grouph, Slvs_Entity entityA, double value) + Slvs_Constraint Slvs_SameOrientation(Slvs_hGroup grouph, Slvs_Entity entityA, Slvs_Entity entityB) + Slvs_Constraint Slvs_Angle(Slvs_hGroup grouph, Slvs_Entity entityA, Slvs_Entity entityB, double value, Slvs_Entity workplane, int inverse) + Slvs_Constraint Slvs_Perpendicular(Slvs_hGroup grouph, Slvs_Entity entityA, Slvs_Entity entityB, Slvs_Entity workplane, int inverse) + Slvs_Constraint Slvs_Parallel(Slvs_hGroup grouph, Slvs_Entity entityA, Slvs_Entity entityB, Slvs_Entity workplane) + Slvs_Constraint Slvs_Tangent(Slvs_hGroup grouph, Slvs_Entity entityA, Slvs_Entity entityB, Slvs_Entity workplane) + Slvs_Constraint Slvs_DistanceProj(Slvs_hGroup grouph, Slvs_Entity ptA, Slvs_Entity ptB, double value) + Slvs_Constraint Slvs_LengthDiff(Slvs_hGroup grouph, Slvs_Entity entityA, Slvs_Entity entityB, double value, Slvs_Entity workplane) + Slvs_Constraint Slvs_Dragged(Slvs_hGroup grouph, Slvs_Entity ptA, Slvs_Entity workplane) + + void Slvs_MarkDragged(Slvs_Entity ptA) + Slvs_SolveResult Slvs_SolveSketch(Slvs_hGroup hg, Slvs_hConstraint **bad) nogil + double Slvs_GetParamValue(int ph) + double Slvs_SetParamValue(int ph, double value) + void Slvs_ClearSketch() + + cdef Slvs_Entity _E_NONE "SLVS_E_NONE" + cdef Slvs_Entity _E_FREE_IN_3D "SLVS_E_FREE_IN_3D" + + cdef int _SLVS_C_POINTS_COINCIDENT "SLVS_C_POINTS_COINCIDENT" + cdef int _SLVS_C_PT_PT_DISTANCE "SLVS_C_PT_PT_DISTANCE" + cdef int _SLVS_C_PT_PLANE_DISTANCE "SLVS_C_PT_PLANE_DISTANCE" + cdef int _SLVS_C_PT_LINE_DISTANCE "SLVS_C_PT_LINE_DISTANCE" + cdef int _SLVS_C_PT_FACE_DISTANCE "SLVS_C_PT_FACE_DISTANCE" + cdef int _SLVS_C_PT_IN_PLANE "SLVS_C_PT_IN_PLANE" + cdef int _SLVS_C_PT_ON_LINE "SLVS_C_PT_ON_LINE" + cdef int _SLVS_C_PT_ON_FACE "SLVS_C_PT_ON_FACE" + cdef int _SLVS_C_EQUAL_LENGTH_LINES "SLVS_C_EQUAL_LENGTH_LINES" + cdef int _SLVS_C_LENGTH_RATIO "SLVS_C_LENGTH_RATIO" + cdef int _SLVS_C_EQ_LEN_PT_LINE_D "SLVS_C_EQ_LEN_PT_LINE_D" + cdef int _SLVS_C_EQ_PT_LN_DISTANCES "SLVS_C_EQ_PT_LN_DISTANCES" + cdef int _SLVS_C_EQUAL_ANGLE "SLVS_C_EQUAL_ANGLE" + cdef int _SLVS_C_EQUAL_LINE_ARC_LEN "SLVS_C_EQUAL_LINE_ARC_LEN" + cdef int _SLVS_C_SYMMETRIC "SLVS_C_SYMMETRIC" + cdef int _SLVS_C_SYMMETRIC_HORIZ "SLVS_C_SYMMETRIC_HORIZ" + cdef int _SLVS_C_SYMMETRIC_VERT "SLVS_C_SYMMETRIC_VERT" + cdef int _SLVS_C_SYMMETRIC_LINE "SLVS_C_SYMMETRIC_LINE" + cdef int _SLVS_C_AT_MIDPOINT "SLVS_C_AT_MIDPOINT" + cdef int _SLVS_C_HORIZONTAL "SLVS_C_HORIZONTAL" + cdef int _SLVS_C_VERTICAL "SLVS_C_VERTICAL" + cdef int _SLVS_C_DIAMETER "SLVS_C_DIAMETER" + cdef int _SLVS_C_PT_ON_CIRCLE "SLVS_C_PT_ON_CIRCLE" + cdef int _SLVS_C_SAME_ORIENTATION "SLVS_C_SAME_ORIENTATION" + cdef int _SLVS_C_ANGLE "SLVS_C_ANGLE" + cdef int _SLVS_C_PARALLEL "SLVS_C_PARALLEL" + cdef int _SLVS_C_PERPENDICULAR "SLVS_C_PERPENDICULAR" + cdef int _SLVS_C_ARC_LINE_TANGENT "SLVS_C_ARC_LINE_TANGENT" + cdef int _SLVS_C_CUBIC_LINE_TANGENT "SLVS_C_CUBIC_LINE_TANGENT" + cdef int _SLVS_C_EQUAL_RADIUS "SLVS_C_EQUAL_RADIUS" + cdef int _SLVS_C_PROJ_PT_DISTANCE "SLVS_C_PROJ_PT_DISTANCE" + cdef int _SLVS_C_WHERE_DRAGGED "SLVS_C_WHERE_DRAGGED" + cdef int _SLVS_C_CURVE_CURVE_TANGENT "SLVS_C_CURVE_CURVE_TANGENT" + cdef int _SLVS_C_LENGTH_DIFFERENCE "SLVS_C_LENGTH_DIFFERENCE" + cdef int _SLVS_C_ARC_ARC_LEN_RATIO "SLVS_C_ARC_ARC_LEN_RATIO" + cdef int _SLVS_C_ARC_LINE_LEN_RATIO "SLVS_C_ARC_LINE_LEN_RATIO" + cdef int _SLVS_C_ARC_ARC_DIFFERENCE "SLVS_C_ARC_ARC_DIFFERENCE" + cdef int _SLVS_C_ARC_LINE_DIFFERENCE "SLVS_C_ARC_LINE_DIFFERENCE" + + cdef int _SLVS_E_POINT_IN_3D "SLVS_E_POINT_IN_3D" + cdef int _SLVS_E_POINT_IN_2D "SLVS_E_POINT_IN_2D" + cdef int _SLVS_E_NORMAL_IN_3D "SLVS_E_NORMAL_IN_3D" + cdef int _SLVS_E_NORMAL_IN_2D "SLVS_E_NORMAL_IN_2D" + cdef int _SLVS_E_DISTANCE "SLVS_E_DISTANCE" + cdef int _SLVS_E_WORKPLANE "SLVS_E_WORKPLANE" + cdef int _SLVS_E_LINE_SEGMENT "SLVS_E_LINE_SEGMENT" + cdef int _SLVS_E_CUBIC "SLVS_E_CUBIC" + cdef int _SLVS_E_CIRCLE "SLVS_E_CIRCLE" + cdef int _SLVS_E_ARC_OF_CIRCLE "SLVS_E_ARC_OF_CIRCLE" + + cdef int _SLVS_RESULT_OKAY "SLVS_RESULT_OKAY" + cdef int _SLVS_RESULT_INCONSISTENT "SLVS_RESULT_INCONSISTENT" + cdef int _SLVS_RESULT_DIDNT_CONVERGE "SLVS_RESULT_DIDNT_CONVERGE" + cdef int _SLVS_RESULT_TOO_MANY_UNKNOWNS "SLVS_RESULT_TOO_MANY_UNKNOWNS" + cdef int _SLVS_RESULT_REDUNDANT_OKAY "SLVS_RESULT_REDUNDANT_OKAY" + +E_NONE = _E_NONE +E_FREE_IN_3D = _E_FREE_IN_3D + +# quaternion +cpdef tuple quaternion_u(double qw, double qx, double qy, double qz): + """Input quaternion, return unit vector of U axis. + Where `qw`, `qx`, `qy`, `qz` are corresponded to the W, X, Y, Z value of + quaternion. + """ + cdef double x, y, z + Slvs_QuaternionU(qw, qx, qy, qz, &x, &y, &z) + return x, y, z + + +cpdef tuple quaternion_v(double qw, double qx, double qy, double qz): + """Input quaternion, return unit vector of V axis. + Signature is same as [quaternion_u](#quaternion_u). + """ + cdef double x, y, z + Slvs_QuaternionV(qw, qx, qy, qz, &x, &y, &z) + return x, y, z + + +cpdef tuple quaternion_n(double qw, double qx, double qy, double qz): + """Input quaternion, return unit vector of normal. + Signature is same as [quaternion_u](#quaternion_u). + """ + cdef double x, y, z + Slvs_QuaternionN(qw, qx, qy, qz, &x, &y, &z) + return x, y, z + + +cpdef tuple make_quaternion(double ux, double uy, double uz, double vx, double vy, double vz): + """Input two unit vector, return quaternion. + Where `ux`, `uy`, `uz` are corresponded to the value of U vector; + `vx`, `vy`, `vz` are corresponded to the value of V vector. + """ + cdef double qw, qx, qy, qz + Slvs_MakeQuaternion(ux, uy, uz, vx, vy, vz, &qw, &qx, &qy, &qz) + return qw, qx, qy, qz + +# entities +def add_point_2d(grouph: int, u: float, v: float, workplane: Slvs_Entity) -> Slvs_Entity: + return Slvs_AddPoint2D(grouph, u, v, workplane) + +def add_point_3d(grouph: int, x: float, y: float, z: float) -> Slvs_Entity: + return Slvs_AddPoint3D(grouph, x, y, z) + +def add_normal_2d(grouph: int, workplane: Slvs_Entity) -> Slvs_Entity: + return Slvs_AddNormal2D(grouph, workplane) + +def add_normal_3d(grouph: int, qw: float, qx: float, qy: float, qz: float) -> Slvs_Entity: + return Slvs_AddNormal3D(grouph, qw, qx, qy, qz) + +def add_distance(grouph: int, value: float, workplane: Slvs_Entity) -> Slvs_Entity: + return Slvs_AddDistance(grouph, value, workplane) + +def add_line_2d(grouph: int, ptA: Slvs_Entity, ptB: Slvs_Entity, workplane: Slvs_Entity) -> Slvs_Entity: + return Slvs_AddLine2D(grouph, ptA, ptB, workplane) + +def add_line_3d(grouph: int, ptA: Slvs_Entity, ptB: Slvs_Entity) -> Slvs_Entity: + return Slvs_AddLine3D(grouph, ptA, ptB) + +def add_cubic(grouph: int, ptA: Slvs_Entity, ptB: Slvs_Entity, ptC: Slvs_Entity, ptD: Slvs_Entity, workplane: Slvs_Entity) -> Slvs_Entity: + return Slvs_AddCubic(grouph, ptA, ptB, ptC, ptD, workplane) + +def add_arc(grouph: int, normal: Slvs_Entity, center: Slvs_Entity, start: Slvs_Entity, end: Slvs_Entity, workplane: Slvs_Entity) -> Slvs_Entity: + return Slvs_AddArc(grouph, normal, center, start, end, workplane) + +def add_circle(grouph: int, normal: Slvs_Entity, center: Slvs_Entity, radius: Slvs_Entity, workplane: Slvs_Entity) -> Slvs_Entity: + return Slvs_AddCircle(grouph, normal, center, radius, workplane) + +def add_workplane(grouph: int, origin: Slvs_Entity, nm: Slvs_Entity) -> Slvs_Entity: + return Slvs_AddWorkplane(grouph, origin, nm) + +def add_base_2d(grouph: int) -> Slvs_Entity: + return Slvs_AddBase2D(grouph) + +# constraints +def add_constraint(grouph: int, c_type: int, workplane: Slvs_Entity, val: float, ptA: Slvs_Entity = E_NONE, + ptB: Slvs_Entity = E_NONE, entityA: Slvs_Entity = E_NONE, + entityB: Slvs_Entity = E_NONE, entityC: Slvs_Entity = E_NONE, + entityD: Slvs_Entity = E_NONE, other: int = 0, other2: int = 0) -> Slvs_Constraint: + return Slvs_AddConstraint(grouph, c_type, workplane, val, ptA, ptB, entityA, entityB, entityC, entityD, other, other2) + +def coincident(grouph: int, entityA: Slvs_Entity, entityB: Slvs_Entity, workplane: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + return Slvs_Coincident(grouph, entityA, entityB, workplane) + +def distance(grouph: int, entityA: Slvs_Entity, entityB: Slvs_Entity, value: float, workplane: Slvs_Entity) -> Slvs_Constraint: + return Slvs_Distance(grouph, entityA, entityB, value, workplane) + +def equal(grouph: int, entityA: Slvs_Entity, entityB: Slvs_Entity, workplane: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + return Slvs_Equal(grouph, entityA, entityB, workplane) + +def equal_angle(grouph: int, entityA: Slvs_Entity, entityB: Slvs_Entity, entityC: Slvs_Entity, + entityD: Slvs_Entity, + workplane: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + return Slvs_EqualAngle(grouph, entityA, entityB, entityC, entityD, workplane) + +def equal_point_to_line(grouph: int, entityA: Slvs_Entity, entityB: Slvs_Entity, + entityC: Slvs_Entity, entityD: Slvs_Entity, + workplane: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + return Slvs_EqualPointToLine(grouph, entityA, entityB, entityC, entityD, workplane) + +def ratio(grouph: int, entityA: Slvs_Entity, entityB: Slvs_Entity, value: float, workplane: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + return Slvs_Ratio(grouph, entityA, entityB, value, workplane) + +def symmetric(grouph: int, entityA: Slvs_Entity, entityB: Slvs_Entity, entityC: Slvs_Entity = E_NONE, workplane: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + return Slvs_Symmetric(grouph, entityA, entityB, entityC, workplane) + +def symmetric_h(grouph: int, ptA: Slvs_Entity, ptB: Slvs_Entity, workplane: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + return Slvs_SymmetricH(grouph, ptA, ptB, workplane) + +def symmetric_v(grouph: int, ptA: Slvs_Entity, ptB: Slvs_Entity, workplane: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + return Slvs_SymmetricV(grouph, ptA, ptB, workplane) + +def midpoint(grouph: int, ptA: Slvs_Entity, ptB: Slvs_Entity, workplane: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + return Slvs_Midpoint(grouph, ptA, ptB, workplane) + +def horizontal(grouph: int, entityA: Slvs_Entity, workplane: Slvs_Entity, entityB: Slvs_Entity = E_NONE) -> Slvs_Constraint: + return Slvs_Horizontal(grouph, entityA, workplane, entityB) + +def vertical(grouph: int, entityA: Slvs_Entity, workplane: Slvs_Entity, entityB: Slvs_Entity = E_NONE) -> Slvs_Constraint: + return Slvs_Vertical(grouph, entityA, workplane, entityB) + +def diameter(grouph: int, entityA: Slvs_Entity, value: float) -> Slvs_Constraint: + return Slvs_Diameter(grouph, entityA, value) + +def same_orientation(grouph: int, entityA: Slvs_Entity, entityB: Slvs_Entity) -> Slvs_Constraint: + return Slvs_SameOrientation(grouph, entityA, entityB) + +def angle(grouph: int, entityA: Slvs_Entity, entityB: Slvs_Entity, value: float, workplane: Slvs_Entity = E_FREE_IN_3D, inverse: bool = False) -> Slvs_Constraint: + return Slvs_Angle(grouph, entityA, entityB, value, workplane, inverse) + +def perpendicular(grouph: int, entityA: Slvs_Entity, entityB: Slvs_Entity, workplane: Slvs_Entity = E_FREE_IN_3D, inverse: bool = False) -> Slvs_Constraint: + return Slvs_Perpendicular(grouph, entityA, entityB, workplane, inverse) + +def parallel(grouph: int, entityA: Slvs_Entity, entityB: Slvs_Entity, workplane: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + return Slvs_Parallel(grouph, entityA, entityB, workplane) + +def tangent(grouph: int, entityA: Slvs_Entity, entityB: Slvs_Entity, workplane: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + return Slvs_Tangent(grouph, entityA, entityB, workplane) + +def distance_proj(grouph: int, ptA: Slvs_Entity, ptB: Slvs_Entity, value: float) -> Slvs_Constraint: + return Slvs_DistanceProj(grouph, ptA, ptB, value) + +def length_diff(grouph: int, entityA: Slvs_Entity, entityB: Slvs_Entity, value: float, workplane: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + return Slvs_LengthDiff(grouph, entityA, entityB, value, workplane) + +def dragged(grouph: int, ptA: Slvs_Entity, workplane: Slvs_Entity = E_FREE_IN_3D) -> Slvs_Constraint: + return Slvs_Dragged(grouph, ptA, workplane) + +# solver +class ResultFlag(IntEnum): + """Symbol of the result flags.""" + OKAY = _SLVS_RESULT_OKAY + INCONSISTENT = _SLVS_RESULT_INCONSISTENT + DIDNT_CONVERGE = _SLVS_RESULT_DIDNT_CONVERGE + TOO_MANY_UNKNOWNS = _SLVS_RESULT_TOO_MANY_UNKNOWNS + REDUNDANT_OKAY = _SLVS_RESULT_REDUNDANT_OKAY + +class ConstraintType(IntEnum): + """Symbol of the constraint types.""" + POINTS_COINCIDENT = _SLVS_C_POINTS_COINCIDENT + PT_PT_DISTANCE = _SLVS_C_PT_PT_DISTANCE + PT_PLANE_DISTANCE = _SLVS_C_PT_PLANE_DISTANCE + PT_LINE_DISTANCE = _SLVS_C_PT_LINE_DISTANCE + PT_FACE_DISTANCE = _SLVS_C_PT_FACE_DISTANCE + PT_IN_PLANE = _SLVS_C_PT_IN_PLANE + PT_ON_LINE = _SLVS_C_PT_ON_LINE + PT_ON_FACE = _SLVS_C_PT_ON_FACE + EQUAL_LENGTH_LINES = _SLVS_C_EQUAL_LENGTH_LINES + LENGTH_RATIO = _SLVS_C_LENGTH_RATIO + EQ_LEN_PT_LINE_D = _SLVS_C_EQ_LEN_PT_LINE_D + EQ_PT_LN_DISTANCES = _SLVS_C_EQ_PT_LN_DISTANCES + EQUAL_ANGLE = _SLVS_C_EQUAL_ANGLE + EQUAL_LINE_ARC_LEN = _SLVS_C_EQUAL_LINE_ARC_LEN + SYMMETRIC = _SLVS_C_SYMMETRIC + SYMMETRIC_HORIZ = _SLVS_C_SYMMETRIC_HORIZ + SYMMETRIC_VERT = _SLVS_C_SYMMETRIC_VERT + SYMMETRIC_LINE = _SLVS_C_SYMMETRIC_LINE + AT_MIDPOINT = _SLVS_C_AT_MIDPOINT + HORIZONTAL = _SLVS_C_HORIZONTAL + VERTICAL = _SLVS_C_VERTICAL + DIAMETER = _SLVS_C_DIAMETER + PT_ON_CIRCLE = _SLVS_C_PT_ON_CIRCLE + SAME_ORIENTATION = _SLVS_C_SAME_ORIENTATION + ANGLE = _SLVS_C_ANGLE + PARALLEL = _SLVS_C_PARALLEL + PERPENDICULAR = _SLVS_C_PERPENDICULAR + ARC_LINE_TANGENT = _SLVS_C_ARC_LINE_TANGENT + CUBIC_LINE_TANGENT = _SLVS_C_CUBIC_LINE_TANGENT + EQUAL_RADIUS = _SLVS_C_EQUAL_RADIUS + PROJ_PT_DISTANCE = _SLVS_C_PROJ_PT_DISTANCE + WHERE_DRAGGED = _SLVS_C_WHERE_DRAGGED + CURVE_CURVE_TANGENT = _SLVS_C_CURVE_CURVE_TANGENT + LENGTH_DIFFERENCE = _SLVS_C_LENGTH_DIFFERENCE + ARC_ARC_LEN_RATIO = _SLVS_C_ARC_ARC_LEN_RATIO + ARC_LINE_LEN_RATIO = _SLVS_C_ARC_LINE_LEN_RATIO + ARC_ARC_DIFFERENCE = _SLVS_C_ARC_ARC_DIFFERENCE + ARC_LINE_DIFFERENCE = _SLVS_C_ARC_LINE_DIFFERENCE + +class EntityType(IntEnum): + POINT_IN_3D = _SLVS_E_POINT_IN_3D + POINT_IN_2D = _SLVS_E_POINT_IN_2D + NORMAL_IN_3D = _SLVS_E_NORMAL_IN_3D + NORMAL_IN_2D = _SLVS_E_NORMAL_IN_2D + DISTANCE = _SLVS_E_DISTANCE + WORKPLANE = _SLVS_E_WORKPLANE + LINE_SEGMENT = _SLVS_E_LINE_SEGMENT + CUBIC = _SLVS_E_CUBIC + CIRCLE = _SLVS_E_CIRCLE + ARC_OF_CIRCLE = _SLVS_E_ARC_OF_CIRCLE + +def mark_dragged(ptA: Slvs_Entity): + Slvs_MarkDragged(ptA) + +def solve_sketch(grouph: int, calculateFaileds: bool): + cdef Slvs_hConstraint *badp = NULL + if not calculateFaileds: + return Slvs_SolveSketch(grouph, NULL) + else: + result = Slvs_SolveSketch(grouph, &badp) + bad = [] + if badp != NULL: + for i in range(0, result.nbad): + bad.append(badp[i]) + free(badp) + return result, bad + +def get_param_value(ph: int): + return Slvs_GetParamValue(ph) + +def set_param_value(ph: int, value: float): + Slvs_SetParamValue(ph, value) + +def clear_sketch(): + Slvs_ClearSketch() diff --git a/src/solvespace.cpp b/src/solvespace.cpp new file mode 100644 index 0000000..887dcbe --- /dev/null +++ b/src/solvespace.cpp @@ -0,0 +1,1236 @@ +//----------------------------------------------------------------------------- +// Entry point in to the program, our registry-stored settings and top-level +// housekeeping when we open, save, and create new files. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" +#include "config.h" + +namespace SolveSpace { + +SolveSpaceUI SS = {}; +Sketch SK = {}; + +void SolveSpaceUI::Init() { +#if !defined(HEADLESS) + // Check that the resource system works. + dbp("%s", LoadString("banner.txt").data()); +#endif + + Platform::SettingsRef settings = Platform::GetSettings(); + + SS.tangentArcRadius = 10.0; + SS.explodeDistance = 1.0; + + // Then, load the registry settings. + // Default list of colors for the model material + modelColor[0] = settings->ThawColor("ModelColor_0", RGBi(150, 150, 150)); + modelColor[1] = settings->ThawColor("ModelColor_1", RGBi(100, 100, 100)); + modelColor[2] = settings->ThawColor("ModelColor_2", RGBi( 30, 30, 30)); + modelColor[3] = settings->ThawColor("ModelColor_3", RGBi(150, 0, 0)); + modelColor[4] = settings->ThawColor("ModelColor_4", RGBi( 0, 100, 0)); + modelColor[5] = settings->ThawColor("ModelColor_5", RGBi( 0, 80, 80)); + modelColor[6] = settings->ThawColor("ModelColor_6", RGBi( 0, 0, 130)); + modelColor[7] = settings->ThawColor("ModelColor_7", RGBi( 80, 0, 80)); + // Light intensities + lightIntensity[0] = settings->ThawFloat("LightIntensity_0", 1.0); + lightIntensity[1] = settings->ThawFloat("LightIntensity_1", 0.5); + ambientIntensity = settings->ThawFloat("Light_Ambient", 0.3); + // Light positions + lightDir[0].x = settings->ThawFloat("LightDir_0_Right", -1.0); + lightDir[0].y = settings->ThawFloat("LightDir_0_Up", 1.0); + lightDir[0].z = settings->ThawFloat("LightDir_0_Forward", 0.0); + lightDir[1].x = settings->ThawFloat("LightDir_1_Right", 1.0); + lightDir[1].y = settings->ThawFloat("LightDir_1_Up", 0.0); + lightDir[1].z = settings->ThawFloat("LightDir_1_Forward", 0.0); + + exportMode = false; + // Chord tolerance + chordTol = settings->ThawFloat("ChordTolerancePct", 0.1); + // Max pwl segments to generate + maxSegments = settings->ThawInt("MaxSegments", 20); + // Chord tolerance + exportChordTol = settings->ThawFloat("ExportChordTolerance", 0.1); + // Max pwl segments to generate + exportMaxSegments = settings->ThawInt("ExportMaxSegments", 64); + // Timeout value for finding redundant constrains (ms) + timeoutRedundantConstr = settings->ThawInt("TimeoutRedundantConstraints", 1000); + // Animation speed calculation base time (ms) + animationSpeed = settings->ThawInt("AnimationSpeed", 800); + // View units + viewUnits = (Unit)settings->ThawInt("ViewUnits", (uint32_t)Unit::MM); + // Number of digits after the decimal point + afterDecimalMm = settings->ThawInt("AfterDecimalMm", 2); + afterDecimalInch = settings->ThawInt("AfterDecimalInch", 3); + afterDecimalDegree = settings->ThawInt("AfterDecimalDegree", 2); + useSIPrefixes = settings->ThawBool("UseSIPrefixes", false); + // Camera tangent (determines perspective) + cameraTangent = settings->ThawFloat("CameraTangent", 0.3f/1e3); + // Grid spacing + gridSpacing = settings->ThawFloat("GridSpacing", 5.0); + // Export scale factor + exportScale = settings->ThawFloat("ExportScale", 1.0); + // Export offset (cutter radius comp) + exportOffset = settings->ThawFloat("ExportOffset", 0.0); + // Dimensions on arcs default to diameter vs radius + arcDimDefaultDiameter = settings->ThawBool("ArcDimDefaultDiameter", false); + // Show full file path in the menu bar + showFullFilePath = settings->ThawBool("ShowFullFilePath", true); + // Rewrite exported colors close to white into black (assuming white bg) + fixExportColors = settings->ThawBool("FixExportColors", true); + // Export background color + exportBackgroundColor = settings->ThawBool("ExportBackgroundColor", false); + // Draw back faces of triangles (when mesh is leaky/self-intersecting) + drawBackFaces = settings->ThawBool("DrawBackFaces", true); + // Use camera mouse navigation + cameraNav = settings->ThawBool("CameraNav", false); + // Use turntable mouse navigation + turntableNav = settings->ThawBool("TurntableNav", false); + // Immediately edit dimension + immediatelyEditDimension = settings->ThawBool("ImmediatelyEditDimension", true); + // Check that contours are closed and not self-intersecting + checkClosedContour = settings->ThawBool("CheckClosedContour", true); + // Enable automatic constrains for lines + automaticLineConstraints = settings->ThawBool("AutomaticLineConstraints", true); + // Draw closed polygons areas + showContourAreas = settings->ThawBool("ShowContourAreas", false); + // Export shaded triangles in a 2d view + exportShadedTriangles = settings->ThawBool("ExportShadedTriangles", true); + // Export pwl curves (instead of exact) always + exportPwlCurves = settings->ThawBool("ExportPwlCurves", false); + // Background color on-screen + backgroundColor = settings->ThawColor("BackgroundColor", RGBi(0, 0, 0)); + // Whether export canvas size is fixed or derived from bbox + exportCanvasSizeAuto = settings->ThawBool("ExportCanvasSizeAuto", true); + // Margins for automatic canvas size + exportMargin.left = settings->ThawFloat("ExportMargin_Left", 5.0); + exportMargin.right = settings->ThawFloat("ExportMargin_Right", 5.0); + exportMargin.bottom = settings->ThawFloat("ExportMargin_Bottom", 5.0); + exportMargin.top = settings->ThawFloat("ExportMargin_Top", 5.0); + // Dimensions for fixed canvas size + exportCanvas.width = settings->ThawFloat("ExportCanvas_Width", 100.0); + exportCanvas.height = settings->ThawFloat("ExportCanvas_Height", 100.0); + exportCanvas.dx = settings->ThawFloat("ExportCanvas_Dx", 5.0); + exportCanvas.dy = settings->ThawFloat("ExportCanvas_Dy", 5.0); + // Extra parameters when exporting G code + gCode.depth = settings->ThawFloat("GCode_Depth", 10.0); + gCode.safeHeight = settings->ThawFloat("GCode_SafeHeight", 5.0); + gCode.passes = settings->ThawInt("GCode_Passes", 1); + gCode.feed = settings->ThawFloat("GCode_Feed", 10.0); + gCode.plungeFeed = settings->ThawFloat("GCode_PlungeFeed", 10.0); + // Show toolbar in the graphics window + showToolbar = settings->ThawBool("ShowToolbar", true); + // Recent files menus + for(size_t i = 0; i < MAX_RECENT; i++) { + std::string rawPath = settings->ThawString("RecentFile_" + std::to_string(i), ""); + if(rawPath.empty()) continue; + recentFiles.push_back(Platform::Path::From(rawPath)); + } + // Autosave timer + autosaveInterval = settings->ThawInt("AutosaveInterval", 5); + // Locale + std::string locale = settings->ThawString("Locale", ""); + if(!locale.empty()) { + SetLocale(locale); + } + + refreshTimer = Platform::CreateTimer(); + refreshTimer->onTimeout = std::bind(&SolveSpaceUI::Refresh, &SS); + + autosaveTimer = Platform::CreateTimer(); + autosaveTimer->onTimeout = std::bind(&SolveSpaceUI::Autosave, &SS); + + // The default styles (colors, line widths, etc.) are also stored in the + // configuration file, but we will automatically load those as we need + // them. + + ScheduleAutosave(); + + NewFile(); + AfterNewFile(); + + if(TW.window && GW.window) { + TW.window->ThawPosition(settings, "TextWindow"); + GW.window->ThawPosition(settings, "GraphicsWindow"); + TW.window->SetVisible(true); + GW.window->SetVisible(true); + GW.window->Focus(); + + // Do this once the window is created. + Request3DConnexionEventsForWindow(GW.window); + } +} + +bool SolveSpaceUI::LoadAutosaveFor(const Platform::Path &filename) { + Platform::Path autosaveFile = filename.WithExtension(BACKUP_EXT); + + FILE *f = OpenFile(autosaveFile, "rb"); + if(!f) + return false; + fclose(f); + + Platform::MessageDialogRef dialog = CreateMessageDialog(GW.window); + + using Platform::MessageDialog; + dialog->SetType(MessageDialog::Type::QUESTION); + dialog->SetTitle(C_("title", "Autosave Available")); + dialog->SetMessage(C_("dialog", "An autosave file is available for this sketch.")); + dialog->SetDescription(C_("dialog", "Do you want to load the autosave file instead?")); + dialog->AddButton(C_("button", "&Load autosave"), MessageDialog::Response::YES, + /*isDefault=*/true); + dialog->AddButton(C_("button", "Do&n't Load"), MessageDialog::Response::NO); + + // FIXME(async): asyncify this call + if(dialog->RunModal() == MessageDialog::Response::YES) { + unsaved = true; + return LoadFromFile(autosaveFile, /*canCancel=*/true); + } + + return false; +} + +bool SolveSpaceUI::Load(const Platform::Path &filename) { + bool autosaveLoaded = LoadAutosaveFor(filename); + bool fileLoaded = autosaveLoaded || LoadFromFile(filename, /*canCancel=*/true); + if(fileLoaded) { + saveFile = filename; + AddToRecentList(filename); + } else { + saveFile.Clear(); + NewFile(); + } + AfterNewFile(); + unsaved = autosaveLoaded; + return fileLoaded; +} + +void SolveSpaceUI::Exit() { + Platform::SettingsRef settings = Platform::GetSettings(); + + GW.window->FreezePosition(settings, "GraphicsWindow"); + TW.window->FreezePosition(settings, "TextWindow"); + + // Recent files + for(size_t i = 0; i < MAX_RECENT; i++) { + std::string rawPath; + if(recentFiles.size() > i) { + rawPath = recentFiles[i].raw; + } + settings->FreezeString("RecentFile_" + std::to_string(i), rawPath); + } + // Model colors + for(size_t i = 0; i < MODEL_COLORS; i++) + settings->FreezeColor("ModelColor_" + std::to_string(i), modelColor[i]); + // Light intensities + settings->FreezeFloat("LightIntensity_0", (float)lightIntensity[0]); + settings->FreezeFloat("LightIntensity_1", (float)lightIntensity[1]); + settings->FreezeFloat("Light_Ambient", (float)ambientIntensity); + // Light directions + settings->FreezeFloat("LightDir_0_Right", (float)lightDir[0].x); + settings->FreezeFloat("LightDir_0_Up", (float)lightDir[0].y); + settings->FreezeFloat("LightDir_0_Forward", (float)lightDir[0].z); + settings->FreezeFloat("LightDir_1_Right", (float)lightDir[1].x); + settings->FreezeFloat("LightDir_1_Up", (float)lightDir[1].y); + settings->FreezeFloat("LightDir_1_Forward", (float)lightDir[1].z); + // Chord tolerance + settings->FreezeFloat("ChordTolerancePct", (float)chordTol); + // Max pwl segments to generate + settings->FreezeInt("MaxSegments", (uint32_t)maxSegments); + // Export Chord tolerance + settings->FreezeFloat("ExportChordTolerance", (float)exportChordTol); + // Export Max pwl segments to generate + settings->FreezeInt("ExportMaxSegments", (uint32_t)exportMaxSegments); + // Timeout for finding which constraints to fix Jacobian + settings->FreezeInt("TimeoutRedundantConstraints", (uint32_t)timeoutRedundantConstr); + // Animation speed + settings->FreezeInt("AnimationSpeed", (uint32_t)animationSpeed); + // View units + settings->FreezeInt("ViewUnits", (uint32_t)viewUnits); + // Number of digits after the decimal point + settings->FreezeInt("AfterDecimalMm", (uint32_t)afterDecimalMm); + settings->FreezeInt("AfterDecimalInch", (uint32_t)afterDecimalInch); + settings->FreezeInt("AfterDecimalDegree", (uint32_t)afterDecimalDegree); + settings->FreezeBool("UseSIPrefixes", useSIPrefixes); + // Camera tangent (determines perspective) + settings->FreezeFloat("CameraTangent", (float)cameraTangent); + // Grid spacing + settings->FreezeFloat("GridSpacing", gridSpacing); + // Export scale + settings->FreezeFloat("ExportScale", exportScale); + // Export offset (cutter radius comp) + settings->FreezeFloat("ExportOffset", exportOffset); + // Rewrite the default arc dimension setting + settings->FreezeBool("ArcDimDefaultDiameter", arcDimDefaultDiameter); + // Show full file path in the menu bar + settings->FreezeBool("ShowFullFilePath", showFullFilePath); + // Rewrite exported colors close to white into black (assuming white bg) + settings->FreezeBool("FixExportColors", fixExportColors); + // Export background color + settings->FreezeBool("ExportBackgroundColor", exportBackgroundColor); + // Draw back faces of triangles (when mesh is leaky/self-intersecting) + settings->FreezeBool("DrawBackFaces", drawBackFaces); + // Draw closed polygons areas + settings->FreezeBool("ShowContourAreas", showContourAreas); + // Check that contours are closed and not self-intersecting + settings->FreezeBool("CheckClosedContour", checkClosedContour); + // Use camera mouse navigation + settings->FreezeBool("CameraNav", cameraNav); + // Use turntable mouse navigation + settings->FreezeBool("TurntableNav", turntableNav); + // Immediately edit dimensions + settings->FreezeBool("ImmediatelyEditDimension", immediatelyEditDimension); + // Enable automatic constrains for lines + settings->FreezeBool("AutomaticLineConstraints", automaticLineConstraints); + // Export shaded triangles in a 2d view + settings->FreezeBool("ExportShadedTriangles", exportShadedTriangles); + // Export pwl curves (instead of exact) always + settings->FreezeBool("ExportPwlCurves", exportPwlCurves); + // Background color on-screen + settings->FreezeColor("BackgroundColor", backgroundColor); + // Whether export canvas size is fixed or derived from bbox + settings->FreezeBool("ExportCanvasSizeAuto", exportCanvasSizeAuto); + // Margins for automatic canvas size + settings->FreezeFloat("ExportMargin_Left", exportMargin.left); + settings->FreezeFloat("ExportMargin_Right", exportMargin.right); + settings->FreezeFloat("ExportMargin_Bottom", exportMargin.bottom); + settings->FreezeFloat("ExportMargin_Top", exportMargin.top); + // Dimensions for fixed canvas size + settings->FreezeFloat("ExportCanvas_Width", exportCanvas.width); + settings->FreezeFloat("ExportCanvas_Height", exportCanvas.height); + settings->FreezeFloat("ExportCanvas_Dx", exportCanvas.dx); + settings->FreezeFloat("ExportCanvas_Dy", exportCanvas.dy); + // Extra parameters when exporting G code + settings->FreezeFloat("GCode_Depth", gCode.depth); + settings->FreezeInt("GCode_Passes", gCode.passes); + settings->FreezeFloat("GCode_Feed", gCode.feed); + settings->FreezeFloat("GCode_PlungeFeed", gCode.plungeFeed); + // Show toolbar in the graphics window + settings->FreezeBool("ShowToolbar", showToolbar); + // Autosave timer + settings->FreezeInt("AutosaveInterval", autosaveInterval); + + // And the default styles, colors and line widths and such. + Style::FreezeDefaultStyles(settings); + + Platform::ExitGui(); +} + +void SolveSpaceUI::Refresh() { + // generateAll must happen bfore updating displays + if(scheduledGenerateAll) { + // Clear the flag so that if the call to GenerateAll is blocked by a Message or Error, + // subsequent refreshes do not try to Generate again. + scheduledGenerateAll = false; + GenerateAll(Generate::DIRTY, /*andFindFree=*/false, /*genForBBox=*/false); + } + if(scheduledShowTW) { + scheduledShowTW = false; + TW.Show(); + } +} + +void SolveSpaceUI::ScheduleGenerateAll() { + scheduledGenerateAll = true; + refreshTimer->RunAfterProcessingEvents(); +} + +void SolveSpaceUI::ScheduleShowTW() { + scheduledShowTW = true; + refreshTimer->RunAfterProcessingEvents(); +} + +void SolveSpaceUI::ScheduleAutosave() { + autosaveTimer->RunAfter(autosaveInterval * 60 * 1000); +} + +double SolveSpaceUI::MmPerUnit() { + switch(viewUnits) { + case Unit::INCHES: return 25.4; + case Unit::FEET_INCHES: return 25.4; // The 'unit' is still inches + case Unit::METERS: return 1000.0; + case Unit::MM: return 1.0; + } + return 1.0; +} +const char *SolveSpaceUI::UnitName() { + switch(viewUnits) { + case Unit::INCHES: return "in"; + case Unit::FEET_INCHES: return "in"; + case Unit::METERS: return "m"; + case Unit::MM: return "mm"; + } + return ""; +} + +std::string SolveSpaceUI::MmToString(double v, bool editable) { + v /= MmPerUnit(); + // The syntax 2' 6" for feet and inches is not something we can (currently) + // parse back from a string so if editable is true, we treat FEET_INCHES the + // same as INCHES and just return the unadorned decimal number of inches. + if(viewUnits == Unit::FEET_INCHES && !editable) { + // Now convert v from inches to 64'ths of an inch, to make rounding easier. + v = floor((v + (1.0 / 128.0)) * 64.0); + int feet = (int)(v / (12.0 * 64.0)); + v = v - (feet * 12.0 * 64.0); + // v is now the feet-less remainder in 1/64 inches + int inches = (int)(v / 64.0); + int numerator = (int)(v - ((double)inches * 64.0)); + int denominator = 64; + // Divide down to smallest denominator where the numerator is still a whole number + while ((numerator != 0) && ((numerator & 1) == 0)) { + numerator /= 2; + denominator /= 2; + } + std::ostringstream str; + if(feet != 0) { + str << feet << "'-"; + } + // For something like 0.5, show 1/2" rather than 0 1/2" + if(!(feet == 0 && inches == 0 && numerator != 0)) { + str << inches; + } + if(numerator != 0) { + str << " " << numerator << "/" << denominator; + } + str << "\""; + return str.str(); + } + + int digits = UnitDigitsAfterDecimal(); + double minimum = 0.5 * pow(10,-digits); + while ((v < minimum) && (v > LENGTH_EPS)) { + digits++; + minimum *= 0.1; + } + return ssprintf("%.*f", digits, v); +} +static const char *DimToString(int dim) { + switch(dim) { + case 3: return "³"; + case 2: return "²"; + case 1: return ""; + default: ssassert(false, "Unexpected dimension"); + } +} +static std::pair SelectSIPrefixMm(int ord, int dim) { +// decide what units to use depending on the order of magnitude of the +// measure in meters and the dimension (1,2,3 lenear, area, volume) + switch(dim) { + case 0: + case 1: + if(ord >= 3) return { 3, "km" }; + else if(ord >= 0) return { 0, "m" }; + else if(ord >= -2) return { -2, "cm" }; + else if(ord >= -3) return { -3, "mm" }; + else if(ord >= -6) return { -6, "µm" }; + else return { -9, "nm" }; + break; + case 2: + if(ord >= 5) return { 3, "km" }; + else if(ord >= 0) return { 0, "m" }; + else if(ord >= -2) return { -2, "cm" }; + else if(ord >= -6) return { -3, "mm" }; + else if(ord >= -13) return { -6, "µm" }; + else return { -9, "nm" }; + break; + case 3: + if(ord >= 7) return { 3, "km" }; + else if(ord >= 0) return { 0, "m" }; + else if(ord >= -5) return { -2, "cm" }; + else if(ord >= -11) return { -3, "mm" }; + else return { -6, "µm" }; + break; + default: + dbp ("dimensions over 3 not supported"); + break; + } + return {0, "m"}; +} +static std::pair SelectSIPrefixInch(int deg) { + if(deg >= 0) return { 0, "in" }; + else if(deg >= -3) return { -3, "mil" }; + else return { -6, "µin" }; +} +std::string SolveSpaceUI::MmToStringSI(double v, int dim) { + bool compact = false; + if(dim == 0) { + if(!useSIPrefixes) return MmToString(v); + compact = true; + dim = 1; + } + + bool inches = (viewUnits == Unit::INCHES) || (viewUnits == Unit::FEET_INCHES); + v /= pow(inches ? 25.4 : 1000, dim); + int vdeg = (int)(log10(fabs(v))); + std::string unit; + if(fabs(v) > 0.0) { + int sdeg = 0; + std::tie(sdeg, unit) = + inches + ? SelectSIPrefixInch(vdeg/dim) + : SelectSIPrefixMm(vdeg, dim); + v /= pow(10.0, sdeg * dim); + } + if(viewUnits == Unit::FEET_INCHES && fabs(v) > pow(12.0, dim)) { + unit = "ft"; + v /= pow(12.0, dim); + } + int pdeg = (int)ceil(log10(fabs(v) + 1e-10)); + return ssprintf("%.*g%s%s%s", pdeg + UnitDigitsAfterDecimal(), v, + compact ? "" : " ", unit.c_str(), DimToString(dim)); +} +std::string SolveSpaceUI::DegreeToString(double v) { + if(fabs(v - floor(v)) > 1e-10) { + return ssprintf("%.*f", afterDecimalDegree, v); + } else { + return ssprintf("%.0f", v); + } +} +double SolveSpaceUI::ExprToMm(Expr *e) { + return (e->Eval()) * MmPerUnit(); +} +double SolveSpaceUI::StringToMm(const std::string &str) { + return std::stod(str) * MmPerUnit(); +} +double SolveSpaceUI::ChordTolMm() { + if(exportMode) return ExportChordTolMm(); + return chordTolCalculated; +} +double SolveSpaceUI::ExportChordTolMm() { + return exportChordTol / exportScale; +} +int SolveSpaceUI::GetMaxSegments() { + if(exportMode) return exportMaxSegments; + return maxSegments; +} +int SolveSpaceUI::UnitDigitsAfterDecimal() { + return (viewUnits == Unit::INCHES || viewUnits == Unit::FEET_INCHES) ? + afterDecimalInch : afterDecimalMm; +} +void SolveSpaceUI::SetUnitDigitsAfterDecimal(int v) { + if(viewUnits == Unit::INCHES || viewUnits == Unit::FEET_INCHES) { + afterDecimalInch = v; + } else { + afterDecimalMm = v; + } +} + +double SolveSpaceUI::CameraTangent() { + if(!usePerspectiveProj) { + return 0; + } else { + return cameraTangent; + } +} + +void SolveSpaceUI::AfterNewFile() { + // Clear out the traced point, which is no longer valid + traced.point = Entity::NO_ENTITY; + traced.path.l.Clear(); + // and the naked edges + nakedEdges.Clear(); + + // Quit export mode + justExportedInfo.draw = false; + centerOfMass.draw = false; + exportMode = false; + + // GenerateAll() expects the view to be valid, because it uses that to + // fill in default values for extrusion depths etc. (which won't matter + // here, but just don't let it work on garbage) + SS.GW.offset = Vector::From(0, 0, 0); + SS.GW.projRight = Vector::From(1, 0, 0); + SS.GW.projUp = Vector::From(0, 1, 0); + + GenerateAll(Generate::ALL); + + GW.Init(); + TW.Init(); + + unsaved = false; + + GW.ZoomToFit(); + + // Create all the default styles; they'll get created on the fly anyways, + // but can't hurt to do it now. + Style::CreateAllDefaultStyles(); + + UpdateWindowTitles(); +} + +void SolveSpaceUI::AddToRecentList(const Platform::Path &filename) { + auto it = std::find_if(recentFiles.begin(), recentFiles.end(), + [&](const Platform::Path &p) { return p.Equals(filename); }); + if(it != recentFiles.end()) { + recentFiles.erase(it); + } + + if(recentFiles.size() > MAX_RECENT) { + recentFiles.erase(recentFiles.begin() + MAX_RECENT); + } + + recentFiles.insert(recentFiles.begin(), filename); + GW.PopulateRecentFiles(); +} + +bool SolveSpaceUI::GetFilenameAndSave(bool saveAs) { + Platform::SettingsRef settings = Platform::GetSettings(); + Platform::Path newSaveFile = saveFile; + + if(saveAs || saveFile.IsEmpty()) { + Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(GW.window); + // FIXME(emscripten): + dbp("Calling AddFilter()..."); + dialog->AddFilter(C_("file-type", "SolveSpace models"), { SKETCH_EXT }); + dbp("Calling ThawChoices()..."); + dialog->ThawChoices(settings, "Sketch"); + if(!newSaveFile.IsEmpty()) { + dbp("Calling SetFilename()..."); + dialog->SetFilename(newSaveFile); + } + dbp("Calling RunModal()..."); + if(dialog->RunModal()) { + dbp("Calling FreezeChoices()..."); + dialog->FreezeChoices(settings, "Sketch"); + newSaveFile = dialog->GetFilename(); + } else { + return false; + } + } + + if(SaveToFile(newSaveFile)) { + AddToRecentList(newSaveFile); + RemoveAutosave(); + saveFile = newSaveFile; + unsaved = false; + if (this->OnSaveFinished) { + this->OnSaveFinished(newSaveFile, saveAs, false); + } + return true; + } else { + return false; + } +} + +void SolveSpaceUI::Autosave() +{ + ScheduleAutosave(); + + if(!saveFile.IsEmpty() && unsaved) { + Platform::Path saveFileName = saveFile.WithExtension(BACKUP_EXT); + SaveToFile(saveFileName); + if (this->OnSaveFinished) { + this->OnSaveFinished(saveFileName, false, true); + } + } +} + +void SolveSpaceUI::RemoveAutosave() +{ + Platform::Path autosaveFile = saveFile.WithExtension(BACKUP_EXT); + RemoveFile(autosaveFile); +} + +bool SolveSpaceUI::OkayToStartNewFile() { + if(!unsaved) return true; + + Platform::MessageDialogRef dialog = CreateMessageDialog(GW.window); + + using Platform::MessageDialog; + dialog->SetType(MessageDialog::Type::QUESTION); + dialog->SetTitle(C_("title", "Modified File")); + if(!SS.saveFile.IsEmpty()) { + dialog->SetMessage(ssprintf(C_("dialog", "Do you want to save the changes you made to " + "the sketch “%s”?"), saveFile.raw.c_str())); + } else { + dialog->SetMessage(C_("dialog", "Do you want to save the changes you made to " + "the new sketch?")); + } + dialog->SetDescription(C_("dialog", "Your changes will be lost if you don't save them.")); + dialog->AddButton(C_("button", "&Save"), MessageDialog::Response::YES, + /*isDefault=*/true); + dialog->AddButton(C_("button", "Do&n't Save"), MessageDialog::Response::NO); + dialog->AddButton(C_("button", "&Cancel"), MessageDialog::Response::CANCEL); + + // FIXME(async): asyncify this call + switch(dialog->RunModal()) { + case MessageDialog::Response::YES: + return GetFilenameAndSave(/*saveAs=*/false); + + case MessageDialog::Response::NO: + RemoveAutosave(); + return true; + + default: + return false; + } +} + +void SolveSpaceUI::UpdateWindowTitles() { + if(!GW.window || !TW.window) return; + + if(saveFile.IsEmpty()) { + GW.window->SetTitle(C_("title", "(new sketch)")); + } else { + if(!GW.window->SetTitleForFilename(saveFile)) { + if(SS.showFullFilePath) { + GW.window->SetTitle(saveFile.raw); + } else { + GW.window->SetTitle(saveFile.raw.substr(saveFile.raw.find_last_of("/\\") + 1)); + } + } + } + + TW.window->SetTitle(C_("title", "Property Browser")); +} + +void SolveSpaceUI::MenuFile(Command id) { + Platform::SettingsRef settings = Platform::GetSettings(); + + switch(id) { + case Command::NEW: + if(!SS.OkayToStartNewFile()) break; + + SS.saveFile.Clear(); + SS.NewFile(); + SS.AfterNewFile(); + break; + + case Command::OPEN: { + if(!SS.OkayToStartNewFile()) break; + + Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window); + dialog->AddFilters(Platform::SolveSpaceModelFileFilters); + dialog->ThawChoices(settings, "Sketch"); + if(dialog->RunModal()) { + dialog->FreezeChoices(settings, "Sketch"); + SS.Load(dialog->GetFilename()); + } + break; + } + + case Command::SAVE: + SS.GetFilenameAndSave(/*saveAs=*/false); + break; + + case Command::SAVE_AS: + SS.GetFilenameAndSave(/*saveAs=*/true); + break; + + case Command::EXPORT_IMAGE: { + Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window); + dialog->AddFilters(Platform::RasterFileFilters); + dialog->ThawChoices(settings, "ExportImage"); + dialog->SuggestFilename(SS.saveFile); + if(dialog->RunModal()) { + dialog->FreezeChoices(settings, "ExportImage"); + SS.ExportAsPngTo(dialog->GetFilename()); + if (SS.OnSaveFinished) { + SS.OnSaveFinished(dialog->GetFilename(), false, false); + } + } + break; + } + + case Command::EXPORT_VIEW: { + Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window); + dialog->AddFilters(Platform::VectorFileFilters); + dialog->ThawChoices(settings, "ExportView"); + dialog->SuggestFilename(SS.saveFile); + if(!dialog->RunModal()) break; + dialog->FreezeChoices(settings, "ExportView"); + + // If the user is exporting something where it would be + // inappropriate to include the constraints, then warn. + if(SS.GW.showConstraints != GraphicsWindow::ShowConstraintMode::SCM_NOSHOW && + (dialog->GetFilename().HasExtension("txt") || fabs(SS.exportOffset) > LENGTH_EPS)) { + Message(_("Constraints are currently shown, and will be exported " + "in the toolpath. This is probably not what you want; " + "hide them by clicking the link at the top of the " + "text window.")); + } + + SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe=*/false); + if (SS.OnSaveFinished) { + SS.OnSaveFinished(dialog->GetFilename(), false, false); + } + break; + } + + case Command::EXPORT_WIREFRAME: { + Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window); + dialog->AddFilters(Platform::Vector3dFileFilters); + dialog->ThawChoices(settings, "ExportWireframe"); + dialog->SuggestFilename(SS.saveFile); + if(!dialog->RunModal()) break; + dialog->FreezeChoices(settings, "ExportWireframe"); + + SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe*/true); + if (SS.OnSaveFinished) { + SS.OnSaveFinished(dialog->GetFilename(), false, false); + } + break; + } + + case Command::EXPORT_SECTION: { + Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window); + dialog->AddFilters(Platform::VectorFileFilters); + dialog->ThawChoices(settings, "ExportSection"); + dialog->SuggestFilename(SS.saveFile); + if(!dialog->RunModal()) break; + dialog->FreezeChoices(settings, "ExportSection"); + + SS.ExportSectionTo(dialog->GetFilename()); + if (SS.OnSaveFinished) { + SS.OnSaveFinished(dialog->GetFilename(), false, false); + } + break; + } + + case Command::EXPORT_MESH: { + Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window); + dialog->AddFilters(Platform::MeshFileFilters); + dialog->ThawChoices(settings, "ExportMesh"); + dialog->SuggestFilename(SS.saveFile); + if(!dialog->RunModal()) break; + dialog->FreezeChoices(settings, "ExportMesh"); + + SS.ExportMeshTo(dialog->GetFilename()); + if (SS.OnSaveFinished) { + SS.OnSaveFinished(dialog->GetFilename(), false, false); + } + + break; + } + + case Command::EXPORT_SURFACES: { + Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window); + dialog->AddFilters(Platform::SurfaceFileFilters); + dialog->ThawChoices(settings, "ExportSurfaces"); + dialog->SuggestFilename(SS.saveFile); + if(!dialog->RunModal()) break; + dialog->FreezeChoices(settings, "ExportSurfaces"); + + StepFileWriter sfw = {}; + sfw.ExportSurfacesTo(dialog->GetFilename()); + if (SS.OnSaveFinished) { + SS.OnSaveFinished(dialog->GetFilename(), false, false); + } + break; + } + + case Command::IMPORT: { + Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window); + dialog->AddFilters(Platform::ImportFileFilters); + dialog->ThawChoices(settings, "Import"); + if(!dialog->RunModal()) break; + dialog->FreezeChoices(settings, "Import"); + + Platform::Path importFile = dialog->GetFilename(); + if(importFile.HasExtension("dxf")) { + ImportDxf(importFile); + } else if(importFile.HasExtension("dwg")) { + ImportDwg(importFile); + } else { + Error(_("Can't identify file type from file extension of " + "filename '%s'; try .dxf or .dwg."), importFile.raw.c_str()); + break; + } + + SS.GenerateAll(SolveSpaceUI::Generate::UNTIL_ACTIVE); + SS.ScheduleShowTW(); + break; + } + + case Command::EXIT: + if(!SS.OkayToStartNewFile()) break; + SS.Exit(); + break; + + default: ssassert(false, "Unexpected menu ID"); + } + + SS.UpdateWindowTitles(); +} + +void SolveSpaceUI::MenuAnalyze(Command id) { + Platform::SettingsRef settings = Platform::GetSettings(); + + SS.GW.GroupSelection(); + auto const &gs = SS.GW.gs; + + switch(id) { + case Command::STEP_DIM: + if(gs.constraints == 1 && gs.n == 0) { + Constraint *c = SK.GetConstraint(gs.constraint[0]); + if(c->HasLabel() && !c->reference) { + SS.TW.stepDim.finish = c->valA; + SS.TW.stepDim.steps = 10; + SS.TW.stepDim.isDistance = + (c->type != Constraint::Type::ANGLE) && + (c->type != Constraint::Type::LENGTH_RATIO) && + (c->type != Constraint::Type::ARC_ARC_LEN_RATIO) && + (c->type != Constraint::Type::ARC_LINE_LEN_RATIO) && + (c->type != Constraint::Type::LENGTH_DIFFERENCE) && + (c->type != Constraint::Type::ARC_ARC_DIFFERENCE) && + (c->type != Constraint::Type::ARC_LINE_DIFFERENCE) ; + SS.TW.shown.constraint = c->h; + SS.TW.shown.screen = TextWindow::Screen::STEP_DIMENSION; + + // The step params are specified in the text window, + // so force that to be shown. + SS.GW.ForceTextWindowShown(); + + SS.ScheduleShowTW(); + SS.GW.ClearSelection(); + } else { + Error(_("Constraint must have a label, and must not be " + "a reference dimension.")); + } + } else { + Error(_("Bad selection for step dimension; select a constraint.")); + } + break; + + case Command::NAKED_EDGES: { + ShowNakedEdges(/*reportOnlyWhenNotOkay=*/false); + break; + } + + case Command::INTERFERENCE: { + SS.nakedEdges.Clear(); + + SMesh *m = &(SK.GetGroup(SS.GW.activeGroup)->displayMesh); + SKdNode *root = SKdNode::From(m); + bool inters, leaks; + root->MakeCertainEdgesInto(&(SS.nakedEdges), + EdgeKind::SELF_INTER, /*coplanarIsInter=*/false, &inters, &leaks); + + SS.GW.Invalidate(); + + if(inters) { + Error("%d edges interfere with other triangles, bad.", + SS.nakedEdges.l.n); + } else { + Message(_("The assembly does not interfere, good.")); + } + break; + } + + case Command::CENTER_OF_MASS: { + SS.UpdateCenterOfMass(); + SS.centerOfMass.draw = true; + SS.GW.Invalidate(); + break; + } + + case Command::VOLUME: { + Group *g = SK.GetGroup(SS.GW.activeGroup); + double totalVol = g->displayMesh.CalculateVolume(); + std::string msg = ssprintf( + _("The volume of the solid model is:\n\n" + " %s"), + SS.MmToStringSI(totalVol, /*dim=*/3).c_str()); + + SMesh curMesh = {}; + g->thisShell.TriangulateInto(&curMesh); + double curVol = curMesh.CalculateVolume(); + if(curVol > 0.0) { + msg += ssprintf( + _("\nThe volume of current group mesh is:\n\n" + " %s"), + SS.MmToStringSI(curVol, /*dim=*/3).c_str()); + } + + msg += _("\n\nCurved surfaces have been approximated as triangles.\n" + "This introduces error, typically of around 1%."); + Message("%s", msg.c_str()); + break; + } + + case Command::AREA: { + Group *g = SK.GetGroup(SS.GW.activeGroup); + SS.GW.GroupSelection(); + + if(gs.faces > 0) { + std::vector faces; + faces.push_back(gs.face[0].v); + if(gs.faces > 1) faces.push_back(gs.face[1].v); + double area = g->displayMesh.CalculateSurfaceArea(faces); + Message(_("The surface area of the selected faces is:\n\n" + " %s\n\n" + "Curves have been approximated as piecewise linear.\n" + "This introduces error, typically of around 1%%."), + SS.MmToStringSI(area, /*dim=*/2).c_str()); + break; + } + + if(g->polyError.how != PolyError::GOOD) { + Error(_("This group does not contain a correctly-formed " + "2d closed area. It is open, not coplanar, or self-" + "intersecting.")); + break; + } + SEdgeList sel = {}; + g->polyLoops.MakeEdgesInto(&sel); + SPolygon sp = {}; + sel.AssemblePolygon(&sp, NULL, /*keepDir=*/true); + sp.normal = sp.ComputeNormal(); + sp.FixContourDirections(); + double area = sp.SignedArea(); + Message(_("The area of the region sketched in this group is:\n\n" + " %s\n\n" + "Curves have been approximated as piecewise linear.\n" + "This introduces error, typically of around 1%%."), + SS.MmToStringSI(area, /*dim=*/2).c_str()); + sel.Clear(); + sp.Clear(); + break; + } + + case Command::PERIMETER: { + if(gs.n > 0 && gs.n == gs.entities) { + double perimeter = 0.0; + for(int i = 0; i < gs.entities; i++) { + Entity *en = SK.entity.FindById(gs.entity[i]); + SEdgeList *el = en->GetOrGenerateEdges(); + for(const SEdge &e : el->l) { + perimeter += e.b.Minus(e.a).Magnitude(); + } + } + Message(_("The total length of the selected entities is:\n\n" + " %s\n\n" + "Curves have been approximated as piecewise linear.\n" + "This introduces error, typically of around 1%%."), + SS.MmToStringSI(perimeter, /*dim=*/1).c_str()); + } else { + Error(_("Bad selection for perimeter; select line segments, arcs, and curves.")); + } + break; + } + + case Command::SHOW_DOF: + // This works like a normal solve, except that it calculates + // which variables are free/bound at the same time. + SS.GenerateAll(SolveSpaceUI::Generate::ALL, /*andFindFree=*/true); + break; + + case Command::TRACE_PT: + if(gs.points == 1 && gs.n == 1) { + SS.traced.point = gs.point[0]; + SS.GW.ClearSelection(); + } else { + Error(_("Bad selection for trace; select a single point.")); + } + break; + + case Command::STOP_TRACING: { + if (SS.traced.point == Entity::NO_ENTITY) { + break; + } + Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window); + dialog->AddFilters(Platform::CsvFileFilters); + dialog->ThawChoices(settings, "Trace"); + dialog->SetFilename(SS.saveFile); + if(dialog->RunModal()) { + dialog->FreezeChoices(settings, "Trace"); + + FILE *f = OpenFile(dialog->GetFilename(), "wb"); + if(f) { + int i; + SContour *sc = &(SS.traced.path); + for(i = 0; i < sc->l.n; i++) { + Vector p = sc->l[i].p; + double s = SS.exportScale; + fprintf(f, "%.10f, %.10f, %.10f\r\n", + p.x/s, p.y/s, p.z/s); + } + fclose(f); + } else { + Error(_("Couldn't write to '%s'"), dialog->GetFilename().raw.c_str()); + } + } + // Clear the trace, and stop tracing + SS.traced.point = Entity::NO_ENTITY; + SS.traced.path.l.Clear(); + SS.GW.Invalidate(); + break; + } + + default: ssassert(false, "Unexpected menu ID"); + } +} + +void SolveSpaceUI::ShowNakedEdges(bool reportOnlyWhenNotOkay) { + SS.nakedEdges.Clear(); + + Group *g = SK.GetGroup(SS.GW.activeGroup); + SMesh *m = &(g->displayMesh); + SKdNode *root = SKdNode::From(m); + bool inters, leaks; + root->MakeCertainEdgesInto(&(SS.nakedEdges), + EdgeKind::NAKED_OR_SELF_INTER, /*coplanarIsInter=*/true, &inters, &leaks); + + if(reportOnlyWhenNotOkay && !inters && !leaks && SS.nakedEdges.l.IsEmpty()) { + return; + } + SS.GW.Invalidate(); + + const char *intersMsg = inters ? + _("The mesh is self-intersecting (NOT okay, invalid).") : + _("The mesh is not self-intersecting (okay, valid)."); + const char *leaksMsg = leaks ? + _("The mesh has naked edges (NOT okay, invalid).") : + _("The mesh is watertight (okay, valid)."); + + std::string cntMsg = ssprintf( + _("\n\nThe model contains %d triangles, from %d surfaces."), + g->displayMesh.l.n, g->runningShell.surface.n); + + if(SS.nakedEdges.l.IsEmpty()) { + Message(_("%s\n\n%s\n\nZero problematic edges, good.%s"), + intersMsg, leaksMsg, cntMsg.c_str()); + } else { + Error(_("%s\n\n%s\n\n%d problematic edges, bad.%s"), + intersMsg, leaksMsg, SS.nakedEdges.l.n, cntMsg.c_str()); + } +} + +void SolveSpaceUI::MenuHelp(Command id) { + switch(id) { + case Command::WEBSITE: + Platform::OpenInBrowser("http://solvespace.com/helpmenu"); + break; + + case Command::ABOUT: + Message(_( +"This is SolveSpace version %s.\n" +"\n" +"For more information, see http://solvespace.com/\n" +"\n" +"SolveSpace is free software: you are free to modify\n" +"and/or redistribute it under the terms of the GNU\n" +"General Public License (GPL) version 3 or later.\n" +"\n" +"There is NO WARRANTY, to the extent permitted by\n" +"law. For details, visit http://gnu.org/licenses/\n" +"\n" +"© 2008-%d Jonathan Westhues and other authors.\n"), +PACKAGE_VERSION, 2026); + break; + + case Command::GITHUB: + Platform::OpenInBrowser(GIT_HASH_URL); + break; + + default: ssassert(false, "Unexpected menu ID"); + } +} + +void SolveSpaceUI::Clear() { + sys.Clear(); + for(int i = 0; i < MAX_UNDO; i++) { + if(i < undo.cnt) undo.d[i].Clear(); + if(i < redo.cnt) redo.d[i].Clear(); + } + TW.window = NULL; + GW.openRecentMenu = NULL; + GW.linkRecentMenu = NULL; + GW.showGridMenuItem = NULL; + GW.dimSolidModelMenuItem = NULL; + GW.perspectiveProjMenuItem = NULL; + GW.explodeMenuItem = NULL; + GW.showToolbarMenuItem = NULL; + GW.showTextWndMenuItem = NULL; + GW.fullScreenMenuItem = NULL; + GW.unitsMmMenuItem = NULL; + GW.unitsMetersMenuItem = NULL; + GW.unitsInchesMenuItem = NULL; + GW.unitsFeetInchesMenuItem = NULL; + GW.inWorkplaneMenuItem = NULL; + GW.in3dMenuItem = NULL; + GW.undoMenuItem = NULL; + GW.redoMenuItem = NULL; + GW.window = NULL; +} + +void Sketch::Clear() { + group.Clear(); + groupOrder.Clear(); + constraint.Clear(); + request.Clear(); + style.Clear(); + entity.Clear(); + param.Clear(); +} + +BBox Sketch::CalculateEntityBBox(bool includingInvisible) { + BBox box = {}; + bool first = true; + + auto includePoint = [&](const Vector &point) { + if(first) { + box.minp = point; + box.maxp = point; + first = false; + } else { + box.Include(point); + } + }; + + for(const Entity &e : entity) { + if(e.construction) continue; + if(!(includingInvisible || e.IsVisible())) continue; + + // arc center point shouldn't be included in bounding box calculation + if(e.IsPoint() && e.h.isFromRequest()) { + Request *r = SK.GetRequest(e.h.request()); + if(r->type == Request::Type::ARC_OF_CIRCLE && e.h == r->h.entity(1)) { + continue; + } + } + + if(e.IsPoint()) { + includePoint(e.PointGetNum()); + continue; + } + + switch(e.type) { + // Circles and arcs are special cases. We calculate their bounds + // based on Bezier curve bounds. This is not exact for arcs, + // but the implementation is rather simple. + case Entity::Type::CIRCLE: + case Entity::Type::ARC_OF_CIRCLE: { + SBezierList sbl = {}; + e.GenerateBezierCurves(&sbl); + + for(const SBezier &sb : sbl.l) { + for(int j = 0; j <= sb.deg; j++) { + includePoint(sb.ctrl[j]); + } + } + sbl.Clear(); + continue; + } + + default: + continue; + } + } + + return box; +} + +Group *Sketch::GetRunningMeshGroupFor(hGroup h) { + Group *g = GetGroup(h); + while(g != NULL) { + if(g->IsMeshGroup()) { + return g; + } + g = g->PreviousGroup(); + } + return NULL; +} + +} // namespace SolveSpace diff --git a/src/solvespace.h b/src/solvespace.h new file mode 100644 index 0000000..572c48d --- /dev/null +++ b/src/solvespace.h @@ -0,0 +1,749 @@ +//----------------------------------------------------------------------------- +// All declarations not grouped specially elsewhere. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- + +#ifndef SOLVESPACE_H +#define SOLVESPACE_H + +#include +#include +#include +#include + +#define EIGEN_NO_DEBUG +#undef Success +#include + +#include "dsc.h" +#include "polygon.h" +#include "srf/surface.h" +#include "render/render.h" +#include "expr.h" +#include "sketch.h" +#include "ttf.h" +#include "ui.h" + +#include "platform/platform.h" + +namespace SolveSpace { + +using std::min; +using std::max; +using std::swap; +using std::fabs; + +enum class Unit : uint32_t { + MM = 0, + INCHES, + METERS, + FEET_INCHES +}; + +class Pixmap; + +enum class SolveResult : uint32_t { + OKAY = 0, + DIDNT_CONVERGE = 10, + REDUNDANT_OKAY = 11, + REDUNDANT_DIDNT_CONVERGE = 12, + TOO_MANY_UNKNOWNS = 20 +}; + +// Utility functions that are provided in the platform-independent code. +class utf8_iterator { + const char *p, *n; +public: + using iterator_category = std::forward_iterator_tag; + using value_type = char32_t; + using difference_type = std::ptrdiff_t; + using pointer = char32_t*; + using reference = char32_t&; + + utf8_iterator(const char *p) : p(p), n(NULL) {} + bool operator==(const utf8_iterator &i) const { return p==i.p; } + bool operator!=(const utf8_iterator &i) const { return p!=i.p; } + ptrdiff_t operator- (const utf8_iterator &i) const { return p -i.p; } + utf8_iterator& operator++() { **this; p=n; n=NULL; return *this; } + utf8_iterator operator++(int) { utf8_iterator t(*this); operator++(); return t; } + char32_t operator*(); + const char* ptr() const { return p; } +}; +class ReadUTF8 { + const std::string &str; +public: + ReadUTF8(const std::string &str) : str(str) {} + utf8_iterator begin() const { return utf8_iterator(&str[0]); } + utf8_iterator end() const { return utf8_iterator(&str[0] + str.length()); } +}; + +class System { +public: + enum { MAX_UNKNOWNS = 2048 }; + + EntityList entity; + ParamList param; + IdList eq; + + // A list of parameters that are being dragged; these are the ones that + // we should put as close as possible to their initial positions. + ParamSet dragged; + + enum { + // In general, the tag indicates the subsys that a variable/equation + // has been assigned to; these are exceptions for variables: + VAR_SUBSTITUTED = 10000, + VAR_DOF_TEST = 10001, + // and for equations: + EQ_SUBSTITUTED = 20000 + }; + + // The system Jacobian matrix + struct { + // The corresponding equation for each row + std::vector eq; + + // The corresponding parameter for each column + std::vector param; + + // We're solving AX = B + int m, n; + struct { + // This only observes the Expr - does not own them! + Eigen::SparseMatrix sym; + Eigen::SparseMatrix num; + } A; + + Eigen::VectorXd X; + + struct { + // This only observes the Expr - does not own them! + std::vector sym; + Eigen::VectorXd num; + } B; + } mat; + + static const double CONVERGE_TOLERANCE; + int CalculateRank(); + bool TestRank(int *dof = NULL, int *rank = NULL); + static bool SolveLinearSystem(const Eigen::SparseMatrix &A, + const Eigen::VectorXd &B, Eigen::VectorXd *X); + bool SolveLeastSquares(); + + bool WriteJacobian(int tag); + void EvalJacobian(); + + void WriteEquationsExceptFor(hConstraint hc, Group *g); + void FindWhichToRemoveToFixJacobian(Group *g, List *bad, + bool forceDofCheck); + SubstitutionMap SolveBySubstitution(); + + bool IsDragged(hParam p); + + bool NewtonSolve(); + + void MarkParamsFree(bool findFree); + + SolveResult Solve(Group *g, int *dof = NULL, List *bad = NULL, + bool andFindBad = false, bool andFindFree = false, + bool forceDofCheck = false); + + SolveResult SolveRank(Group *g, int *rank = NULL, int *dof = NULL, + List *bad = NULL, + bool andFindBad = false, bool andFindFree = false); + + void Clear(); +}; + +class StepFileWriter { +public: + bool HasCartesianPointAnAlias(int number, Vector v, int vertex, + bool *vertex_has_alias = nullptr); + int InsertPoint(int number); + int InsertVertex(int number); + bool HasBSplineCurveAnAlias(int number, std::vector points); + int InsertCurve(int number); + bool HasEdgeCurveAnAlias(int number, int prevFinish, int thisFinish, int curveId, + bool *flip = nullptr); + int InsertEdgeCurve(int number); + bool HasOrientedEdgeAnAlias(int number, int edgeCurveId, bool flip); + int InsertOrientedEdge(int number); + void ExportSurfacesTo(const Platform::Path &filename); + void WriteHeader(); + void WriteProductHeader(); + int ExportCurve(SBezier *sb); + int ExportCurveLoop(SBezierLoop *loop, bool inner); + void ExportSurface(SSurface *ss, SBezierList *sbl); + void WriteWireframe(); + void WriteFooter(); + + List curves; + List advancedFaces; + FILE *f; + int id; + + // Structs to keep track of duplicated entities. + // Basic alias. + typedef struct { + int reference; + std::vector aliases; + } alias_t; + + // Cartesian points. + typedef struct { + alias_t alias; + alias_t vertexAlias; + Vector v; + } pointAliases_t; + + // Curves. + typedef struct { + alias_t alias; + std::vector memberPoints; + RgbaColor color; + } curveAliases_t; + + // Edges. + typedef struct { + alias_t alias; + int prevFinish; + int thisFinish; + int curveId; + RgbaColor color; + } edgeCurveAliases_t; + + // Edges. + typedef struct { + alias_t alias; + int edgeCurveId; + bool flip; + } orientedEdgeAliases_t; + + std::vector pointAliases; + std::vector edgeCurveAliases; + std::vector curveAliases; + std::vector orientedEdgeAliases; + bool exportParts = true; + RgbaColor currentColor; +}; + +class VectorFileWriter { +protected: + Vector u, v, n, origin; + double cameraTan, scale; + +public: + FILE *f; + Platform::Path filename; + Vector ptMin, ptMax; + + static double MmToPts(double mm); + + static VectorFileWriter *ForFile(const Platform::Path &filename); + + void SetModelviewProjection(const Vector &u, const Vector &v, const Vector &n, + const Vector &origin, double cameraTan, double scale); + Vector Transform(Vector &pos) const; + + void OutputLinesAndMesh(SBezierLoopSetSet *sblss, SMesh *sm); + + void BezierAsPwl(SBezier *sb); + void BezierAsNonrationalCubic(SBezier *sb, int depth=0); + + virtual void StartPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) = 0; + virtual void FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) = 0; + virtual void Bezier(SBezier *sb) = 0; + virtual void Triangle(STriangle *tr) = 0; + virtual bool OutputConstraints(IdList *) { return false; } + virtual void Background(RgbaColor color) = 0; + virtual void StartFile() = 0; + virtual void FinishAndCloseFile() = 0; + virtual bool HasCanvasSize() const = 0; + virtual bool CanOutputMesh() const = 0; +}; +class DxfFileWriter : public VectorFileWriter { +public: + struct BezierPath { + std::vector beziers; + }; + + std::vector paths; + IdList *constraint; + + static const char *lineTypeName(StipplePattern stippleType); + + bool OutputConstraints(IdList *constraint) override; + + void StartPath( RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void Triangle(STriangle *tr) override; + void Bezier(SBezier *sb) override; + void Background(RgbaColor color) override; + void StartFile() override; + void FinishAndCloseFile() override; + bool HasCanvasSize() const override { return false; } + bool CanOutputMesh() const override { return false; } + bool NeedToOutput(Constraint *c); +}; +class EpsFileWriter : public VectorFileWriter { +public: + Vector prevPt; + void MaybeMoveTo(Vector s, Vector f); + + void StartPath( RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void Triangle(STriangle *tr) override; + void Bezier(SBezier *sb) override; + void Background(RgbaColor color) override; + void StartFile() override; + void FinishAndCloseFile() override; + bool HasCanvasSize() const override { return true; } + bool CanOutputMesh() const override { return true; } +}; +class PdfFileWriter : public VectorFileWriter { +public: + uint32_t xref[10]; + uint32_t bodyStart; + Vector prevPt; + void MaybeMoveTo(Vector s, Vector f); + + void StartPath( RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void Triangle(STriangle *tr) override; + void Bezier(SBezier *sb) override; + void Background(RgbaColor color) override; + void StartFile() override; + void FinishAndCloseFile() override; + bool HasCanvasSize() const override { return true; } + bool CanOutputMesh() const override { return true; } +}; +class SvgFileWriter : public VectorFileWriter { +public: + Vector prevPt; + void MaybeMoveTo(Vector s, Vector f); + + void StartPath( RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void Triangle(STriangle *tr) override; + void Bezier(SBezier *sb) override; + void Background(RgbaColor color) override; + void StartFile() override; + void FinishAndCloseFile() override; + bool HasCanvasSize() const override { return true; } + bool CanOutputMesh() const override { return true; } +}; +class HpglFileWriter : public VectorFileWriter { +public: + static double MmToHpglUnits(double mm); + void StartPath( RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void Triangle(STriangle *tr) override; + void Bezier(SBezier *sb) override; + void Background(RgbaColor color) override; + void StartFile() override; + void FinishAndCloseFile() override; + bool HasCanvasSize() const override { return false; } + bool CanOutputMesh() const override { return false; } +}; +class Step2dFileWriter : public VectorFileWriter { + StepFileWriter sfw; + void StartPath( RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void Triangle(STriangle *tr) override; + void Bezier(SBezier *sb) override; + void Background(RgbaColor color) override; + void StartFile() override; + void FinishAndCloseFile() override; + bool HasCanvasSize() const override { return false; } + bool CanOutputMesh() const override { return false; } +}; +class GCodeFileWriter : public VectorFileWriter { +public: + SEdgeList sel; + void StartPath( RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void FinishPath(RgbaColor strokeRgb, double lineWidth, + bool filled, RgbaColor fillRgb, hStyle hs) override; + void Triangle(STriangle *tr) override; + void Bezier(SBezier *sb) override; + void Background(RgbaColor color) override; + void StartFile() override; + void FinishAndCloseFile() override; + bool HasCanvasSize() const override { return false; } + bool CanOutputMesh() const override { return false; } +}; + +#ifdef LIBRARY +# define ENTITY EntityBase +# define CONSTRAINT ConstraintBase +#else +# define ENTITY Entity +# define CONSTRAINT Constraint +#endif +class Sketch { +public: + // These are user-editable, and define the sketch. + IdList group; + List
groupOrder; + IdList constraint; + IdList request; + IdList style; + + // These are generated from the above. + IdList entity; + ParamList param; + + inline CONSTRAINT *GetConstraint(hConstraint h) + { return constraint.FindById(h); } + inline ENTITY *GetEntity (hEntity h) { return entity. FindById(h); } + inline Param *GetParam (hParam h) { return param. FindById(h); } + inline Request *GetRequest(hRequest h) { return request.FindById(h); } + inline Group *GetGroup (hGroup h) { return group. FindById(h); } + // Styles are handled a bit differently. + + void Clear(); + + BBox CalculateEntityBBox(bool includingInvisible); + Group *GetRunningMeshGroupFor(hGroup h); +}; +#undef ENTITY +#undef CONSTRAINT + +class SolveSpaceUI { +public: + TextWindow *pTW; + TextWindow &TW; + GraphicsWindow GW; + + // The state for undo/redo + typedef struct UndoState { + IdList group; + List
groupOrder; + IdList request; + IdList constraint; + ParamList param; + IdList style; + hGroup activeGroup; + + void Clear() { + group.Clear(); + request.Clear(); + constraint.Clear(); + param.Clear(); + style.Clear(); + } + } UndoState; + enum { MAX_UNDO = 100 }; + typedef struct { + UndoState d[MAX_UNDO]; + int cnt; + int write; + } UndoStack; + UndoStack undo; + UndoStack redo; + + std::map, Platform::PathLess> images; + bool ReloadLinkedImage(const Platform::Path &saveFile, Platform::Path *filename, + bool canCancel); + + void UndoEnableMenus(); + void UndoRemember(); + void UndoUndo(); + void UndoRedo(); + void PushFromCurrentOnto(UndoStack *uk); + void PopOntoCurrentFrom(UndoStack *uk); + void UndoClearState(UndoState *ut); + void UndoClearStack(UndoStack *uk); + + // Little bits of extra configuration state + enum { MODEL_COLORS = 8 }; + RgbaColor modelColor[MODEL_COLORS]; + Vector lightDir[2]; + double lightIntensity[2]; + double ambientIntensity; + double chordTol; + double chordTolCalculated; + int maxSegments; + double exportChordTol; + int exportMaxSegments; + int timeoutRedundantConstr; //milliseconds + int animationSpeed; //milliseconds + double cameraTangent; + double gridSpacing; + double exportScale; + double exportOffset; + bool arcDimDefaultDiameter; + bool showFullFilePath; + bool fixExportColors; + bool exportBackgroundColor; + bool drawBackFaces; + bool showContourAreas; + bool checkClosedContour; + bool cameraNav; + bool turntableNav; + bool immediatelyEditDimension; + bool automaticLineConstraints; + bool showToolbar; + Platform::Path screenshotFile; + RgbaColor backgroundColor; + bool exportShadedTriangles; + bool exportPwlCurves; + bool exportCanvasSizeAuto; + bool exportMode; + struct { + double left; + double right; + double bottom; + double top; + } exportMargin; + struct { + double width; + double height; + double dx; + double dy; + } exportCanvas; + struct { + double depth; + double safeHeight; + int passes; + double feed; + double plungeFeed; + } gCode; + + Unit viewUnits; + int afterDecimalMm; + int afterDecimalInch; + int afterDecimalDegree; + bool useSIPrefixes; + int autosaveInterval; // in minutes + bool explode; + double explodeDistance; + + std::string MmToString(double v, bool editable=false); + std::string MmToStringSI(double v, int dim = 0); + std::string DegreeToString(double v); + double ExprToMm(Expr *e); + double StringToMm(const std::string &s); + const char *UnitName(); + double MmPerUnit(); + int UnitDigitsAfterDecimal(); + void SetUnitDigitsAfterDecimal(int v); + double ChordTolMm(); + double ExportChordTolMm(); + int GetMaxSegments(); + bool usePerspectiveProj; + double CameraTangent(); + + // Some stuff relating to the tangent arcs created non-parametrically + // as special requests. + double tangentArcRadius; + bool tangentArcManual; + bool tangentArcModify; + + // The platform-dependent code calls this before entering the msg loop + void Init(); + void Exit(); + + // File load/save routines, including the additional files that get + // loaded when we have link groups. + FILE *fh; + void AfterNewFile(); + void AddToRecentList(const Platform::Path &filename); + Platform::Path saveFile; + bool fileLoadError; + bool unsaved; + typedef struct { + char type; + const char *desc; + char fmt; + void *ptr; + } SaveTable; + static const SaveTable SAVED[]; + void SaveUsingTable(const Platform::Path &filename, int type); + void LoadUsingTable(const Platform::Path &filename, char *key, char *val); + struct { + Group g; + Request r; + Entity e; + Param p; + Constraint c; + Style s; + } sv; + static void MenuFile(Command id); + void Autosave(); + void RemoveAutosave(); + static constexpr size_t MAX_RECENT = 8; + static constexpr const char *SKETCH_EXT = "slvs"; + static constexpr const char *BACKUP_EXT = "slvs~"; + std::vector recentFiles; + bool Load(const Platform::Path &filename); + bool GetFilenameAndSave(bool saveAs); + bool OkayToStartNewFile(); + hGroup CreateDefaultDrawingGroup(); + void UpdateWindowTitles(); + void ClearExisting(); + void NewFile(); + bool SaveToFile(const Platform::Path &filename); + bool LoadAutosaveFor(const Platform::Path &filename); + std::function OnSaveFinished; + bool LoadFromFile(const Platform::Path &filename, bool canCancel = false); + void UpgradeLegacyData(); + bool LoadEntitiesFromFile(const Platform::Path &filename, EntityList *le, + SMesh *m, SShell *sh); + bool LoadEntitiesFromSlvs(const Platform::Path &filename, EntityList *le, + SMesh *m, SShell *sh); + bool ReloadAllLinked(const Platform::Path &filename, bool canCancel = false); + // And the various export options + void ExportAsPngTo(const Platform::Path &filename); + void ExportMeshTo(const Platform::Path &filename); + void ExportMeshAsStlTo(FILE *f, SMesh *sm); + void ExportMeshAsObjTo(FILE *fObj, FILE *fMtl, SMesh *sm); + void ExportMeshAsThreeJsTo(FILE *f, const Platform::Path &filename, + SMesh *sm, SOutlineList *sol); + void ExportMeshAsVrmlTo(FILE *f, const Platform::Path &filename, SMesh *sm); + void ExportViewOrWireframeTo(const Platform::Path &filename, bool exportWireframe); + void ExportSectionTo(const Platform::Path &filename); + void ExportWireframeCurves(SEdgeList *sel, SBezierList *sbl, + VectorFileWriter *out); + void ExportLinesAndMesh(SEdgeList *sel, SBezierList *sbl, SMesh *sm, + Vector u, Vector v, + Vector n, Vector origin, + double cameraTan, + VectorFileWriter *out); + + static void MenuAnalyze(Command id); + + // Additional display stuff + struct { + SContour path; + hEntity point; + } traced; + SEdgeList nakedEdges; + struct { + bool draw; + Vector ptA; + Vector ptB; + } extraLine; + struct { + bool draw, showOrigin; + Vector pt, u, v; + } justExportedInfo; + struct { + bool draw; + bool dirty; + Vector position; + } centerOfMass; + + class Clipboard { + public: + List r; + List c; + + void Clear(); + bool ContainsEntity(hEntity old); + hEntity NewEntityFor(hEntity old); + }; + Clipboard clipboard; + + void MarkGroupDirty(hGroup hg, bool onlyThis = false); + void MarkGroupDirtyByEntity(hEntity he); + + // Consistency checking on the sketch: stuff with missing dependencies + // will get deleted automatically. + struct { + int requests; + int groups; + int constraints; + int nonTrivialConstraints; + } deleted; + bool GroupExists(hGroup hg); + bool PruneOrphans(); + bool EntityExists(hEntity he); + bool GroupsInOrder(hGroup before, hGroup after); + bool PruneGroups(hGroup hg); + bool PruneRequestsAndConstraints(hGroup hg); + static void ShowNakedEdges(bool reportOnlyWhenNotOkay); + + enum class Generate : uint32_t { + DIRTY, + ALL, + REGEN, + UNTIL_ACTIVE, + }; + + void GenerateAll(Generate type = Generate::DIRTY, bool andFindFree = false, + bool genForBBox = false); + void SolveGroup(hGroup hg, bool andFindFree); + void SolveGroupAndReport(hGroup hg, bool andFindFree); + SolveResult TestRankForGroup(hGroup hg, int *rank = NULL); + void WriteEqSystemForGroup(hGroup hg); + void MarkDraggedParams(); + void ForceReferences(); + void UpdateCenterOfMass(); + + bool ActiveGroupsOkay(); + + // The system to be solved. + System *pSys; + System &sys; + + // All the TrueType fonts in memory + TtfFontList fonts; + + // Everything has been pruned, so we know there's no dangling references + // to entities that don't exist. Before that, we mustn't try to display + // the sketch! + bool allConsistent; + + bool scheduledGenerateAll; + bool scheduledShowTW; + Platform::TimerRef refreshTimer; + Platform::TimerRef autosaveTimer; + void Refresh(); + void ScheduleShowTW(); + void ScheduleGenerateAll(); + void ScheduleAutosave(); + + static void MenuHelp(Command id); + + void Clear(); + + // We allocate TW and sys on the heap to work around an MSVC problem + // where it puts zero-initialized global data in the binary (~30M of zeroes) + // in release builds. + SolveSpaceUI() + : pTW(new TextWindow()), TW(*pTW), + pSys(new System()), sys(*pSys) {} + + ~SolveSpaceUI() { + delete pTW; + delete pSys; + } +}; + +void ImportDxf(const Platform::Path &file); +void ImportDwg(const Platform::Path &file); +bool LinkIDF(const Platform::Path &filename, EntityList *le, SMesh *m, SShell *sh); +bool LinkStl(const Platform::Path &filename, EntityList *le, SMesh *m, SShell *sh); + +extern SolveSpaceUI SS; +extern Sketch SK; + +} // namespace SolveSpace + +#endif diff --git a/src/srf/boolean.cpp b/src/srf/boolean.cpp new file mode 100644 index 0000000..4778b18 --- /dev/null +++ b/src/srf/boolean.cpp @@ -0,0 +1,1049 @@ +//----------------------------------------------------------------------------- +// Top-level functions to compute the Boolean union, difference or intersection +// between two shells of rational polynomial surfaces. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +static int I; + +void SShell::MakeFromUnionOf(SShell *a, SShell *b) { + MakeFromBoolean(a, b, SSurface::CombineAs::UNION); +} + +void SShell::MakeFromDifferenceOf(SShell *a, SShell *b) { + MakeFromBoolean(a, b, SSurface::CombineAs::DIFFERENCE); +} + +void SShell::MakeFromIntersectionOf(SShell *a, SShell *b) { + MakeFromBoolean(a, b, SSurface::CombineAs::INTERSECTION); +} + +void SCurve::GetAxisAlignedBounding(Vector *ptMax, Vector *ptMin) const { + *ptMax = {VERY_NEGATIVE, VERY_NEGATIVE, VERY_NEGATIVE}; + *ptMin = {VERY_POSITIVE, VERY_POSITIVE, VERY_POSITIVE}; + + for(int i = 0; i <= exact.deg; i++) { + exact.ctrl[i].MakeMaxMin(ptMax, ptMin); + } +} + +// We will be inserting other curve vertices into our curves to split them. +// This is helpful when curved surfaces become tangent along a trim and the +// usual tests for curve-surface intersection don't split the curve at a vertex. +// This is faster than the previous version that split at surface corners and +// handles more buggy cases. It's not clear this is the best way but it works ok. +static void FindVertsOnCurve(List *l, const SCurve *curve, SShell *sh) { + + Vector amax, amin; + curve->GetAxisAlignedBounding(&amax, &amin); + + for(const auto &sc : sh->curve) { + if(!sc.isExact) continue; + + Vector cmax, cmin; + sc.GetAxisAlignedBounding(&cmax, &cmin); + + if(Vector::BoundingBoxesDisjoint(amax, amin, cmax, cmin)) { + // They cannot possibly intersect, no curves to generate + continue; + } + + for(int i=0; i<2; i++) { + Vector pt = sc.exact.ctrl[ i==0 ? 0 : sc.exact.deg ]; + double t; + curve->exact.ClosestPointTo(pt, &t, /*must converge=*/ false); + double d = pt.Minus(curve->exact.PointAt(t)).Magnitude(); + if((t>LENGTH_EPS) && (t<(1.0-LENGTH_EPS)) && (d < LENGTH_EPS)) { + SInter inter; + inter.p = pt; + l->Add(&inter); + } + } + } +} + +//----------------------------------------------------------------------------- +// Take our original pwl curve. Wherever an edge intersects a surface within +// either agnstA or agnstB, split the piecewise linear element. Then refine +// the intersection so that it lies on all three relevant surfaces: the +// intersecting surface, srfA, and srfB. (So the pwl curve should lie at +// the intersection of srfA and srfB.) Return a new pwl curve with everything +// split. +//----------------------------------------------------------------------------- +SCurve SCurve::MakeCopySplitAgainst(SShell *agnstA, SShell *agnstB, + SSurface *srfA, SSurface *srfB) const +{ + SCurve ret; + ret = *this; + ret.pts = {}; + + // First find any vertex that lies on our curve. + List vertpts = {}; + if(isExact) { + if(agnstA) + FindVertsOnCurve(&vertpts, this, agnstA); + if(agnstB) + FindVertsOnCurve(&vertpts, this, agnstB); + } + + const SCurvePt *p = pts.First(); + ssassert(p != NULL, "Cannot split an empty curve"); + SCurvePt prev = *p; + ret.pts.Add(p); + p = pts.NextAfter(p); + + for(; p; p = pts.NextAfter(p)) { + List il = {}; + + // Find all the intersections with the two passed shells + if(agnstA) + agnstA->AllPointsIntersecting(prev.p, p->p, &il, + /*asSegment=*/true, /*trimmed=*/false, /*inclTangent=*/true); + if(agnstB) + agnstB->AllPointsIntersecting(prev.p, p->p, &il, + /*asSegment=*/true, /*trimmed=*/false, /*inclTangent=*/true); + + if(!il.IsEmpty()) { + // The intersections were generated by intersecting the pwl + // edge against a surface; so they must be refined to lie + // exactly on the original curve. + il.ClearTags(); + SInter *pi; + for(pi = il.First(); pi; pi = il.NextAfter(pi)) { + if(pi->srf == srfA || pi->srf == srfB) { + // The edge certainly intersects the surfaces that it + // trims (at its endpoints), but those ones don't count. + // They are culled later, but no sense calculating them + // and they will cause numerical problems (since two + // of the three surfaces they're refined to lie on will + // be identical, so the matrix will be singular). + pi->tag = 1; + continue; + } + + Point2d puv; + (pi->srf)->ClosestPointTo(pi->p, &puv, /*mustConverge=*/false); + + // Split the edge if the intersection lies within the surface's + // trim curves, or within the chord tol of the trim curve; want + // some slop if points are close to edge and pwl is too coarse, + // and it doesn't hurt to split unnecessarily. + Point2d dummy = { 0, 0 }; + SBspUv::Class c = (pi->srf->bsp) ? pi->srf->bsp->ClassifyPoint(puv, dummy, pi->srf) : SBspUv::Class::OUTSIDE; + if(c == SBspUv::Class::OUTSIDE) { + double d = VERY_POSITIVE; + if(pi->srf->bsp) d = pi->srf->bsp->MinimumDistanceToEdge(puv, pi->srf); + if(d > SS.ChordTolMm()) { + pi->tag = 1; + continue; + } + } + + // We're keeping the intersection, so actually refine it. Finding the intersection + // to within EPS is important to match the ends of different chopped trim curves. + // The general 3-surface intersection fails to refine for trims where surfaces + // are tangent at the curve, but those trims are usually exact, so… + if(isExact) { + (pi->srf)->PointOnCurve(&exact, &(puv.x), &(puv.y)); + } else { + (pi->srf)->PointOnSurfaces(srfA, srfB, &(puv.x), &(puv.y)); + } + pi->p = (pi->srf)->PointAt(puv); + } + il.RemoveTagged(); + } + // Now add any vertex that is on this segment + const Vector lineStart = prev.p; + const Vector lineDirection = (p->p).Minus(prev.p); + for(const auto &vtx : vertpts) { + double t = (vtx.p.Minus(lineStart)).DivProjected(lineDirection); + if((0.0 < t) && (t < 1.0)) { + il.Add(&vtx); + } + } + if(!il.IsEmpty()) { + SInter *pi; + + // And now sort them in order along the line. Note that we must + // do that after refining, in case the refining would make two + // points switch places. + std::sort(il.begin(), il.end(), [&](const SInter &a, const SInter &b) { + double ta = (a.p.Minus(lineStart)).DivProjected(lineDirection); + double tb = (b.p.Minus(lineStart)).DivProjected(lineDirection); + + return (ta < tb); + }); + + // And now uses the intersections to generate our split pwl edge(s) + Vector prev = Vector::From(VERY_POSITIVE, 0, 0); + for(pi = il.First(); pi; pi = il.NextAfter(pi)) { + // On-edge intersection will generate same split point for + // both surfaces, so don't create zero-length edge. + if(!prev.Equals(pi->p)) { + SCurvePt scpt; + scpt.tag = 0; + scpt.p = pi->p; + scpt.vertex = true; + ret.pts.Add(&scpt); + } + prev = pi->p; + } + } + + il.Clear(); + ret.pts.Add(p); + prev = *p; + } + vertpts.Clear(); + return ret; +} + +void SShell::CopyCurvesSplitAgainst(bool opA, SShell *agnst, SShell *into) { +#pragma omp parallel for + for(int i=0; iMakeCopySplitAgainst(agnst, NULL, + surface.FindById(sc->surfA), + surface.FindById(sc->surfB)); + scn.source = opA ? SCurve::Source::A : SCurve::Source::B; +#pragma omp critical + { + hSCurve hsc = into->curve.AddAndAssignId(&scn); + // And note the new ID so that we can rewrite the trims appropriately + sc->newH = hsc; + } + } +} + +void SSurface::TrimFromEdgeList(SEdgeList *el, bool asUv) { + el->l.ClearTags(); + + STrimBy stb = {}; + for(;;) { + // Find an edge, any edge; we'll start from there. + SEdge *se; + for(se = el->l.First(); se; se = el->l.NextAfter(se)) { + if(se->tag) continue; + break; + } + if(!se) break; + se->tag = 1; + stb.start = se->a; + stb.finish = se->b; + stb.curve.v = se->auxA; + stb.backwards = se->auxB ? true : false; + + // Find adjoining edges from the same curve; those should be + // merged into a single trim. + bool merged; + do { + merged = false; + for(se = el->l.First(); se; se = el->l.NextAfter(se)) { + if(se->tag) continue; + if(se->auxA != (int)stb.curve.v) continue; + if(( se->auxB && !stb.backwards) || + (!se->auxB && stb.backwards)) continue; + + if((se->a).Equals(stb.finish)) { + stb.finish = se->b; + se->tag = 1; + merged = true; + } else if((se->b).Equals(stb.start)) { + stb.start = se->a; + se->tag = 1; + merged = true; + } + } + } while(merged); + + if(asUv) { + stb.start = PointAt(stb.start.x, stb.start.y); + stb.finish = PointAt(stb.finish.x, stb.finish.y); + } + + // And add the merged trim, with xyz (not uv like the polygon) pts + trim.Add(&stb); + } +} + +static bool KeepRegion(SSurface::CombineAs type, bool opA, SShell::Class shell, SShell::Class orig) +{ + bool inShell = (shell == SShell::Class::SURF_INSIDE), + outSide = (shell == SShell::Class::SURF_OUTSIDE), + coincSame = (shell == SShell::Class::SURF_COINC_SAME), + coincOpp = (shell == SShell::Class::SURF_COINC_OPP), + inOrig = (orig == SShell::Class::SURF_INSIDE); + + // This one line is not really part of this functions logic + if(!inOrig) return false; + + switch(type) { + case SSurface::CombineAs::UNION: + if(opA) { + return outSide; + } else { + return outSide || coincSame; + } + + case SSurface::CombineAs::DIFFERENCE: + if(opA) { + return outSide || coincOpp; + } else { + return inShell; + } + + case SSurface::CombineAs::INTERSECTION: + if(opA) { + return inShell; + } else { + return inShell || coincSame; + } + + default: ssassert(false, "Unexpected combine type"); + } +} +static bool KeepEdge(SSurface::CombineAs type, bool opA, + SShell::Class indir_shell, SShell::Class outdir_shell, + SShell::Class indir_orig, SShell::Class outdir_orig) +{ + bool keepIn = KeepRegion(type, opA, indir_shell, indir_orig), + keepOut = KeepRegion(type, opA, outdir_shell, outdir_orig); + + // If the regions to the left and right of this edge are both in or both + // out, then this edge is not useful and should be discarded. + if(keepIn && !keepOut) return true; + return false; +} + +static void TagByClassifiedEdge(SBspUv::Class bspclass, SShell::Class *indir, SShell::Class *outdir) +{ + switch(bspclass) { + case SBspUv::Class::INSIDE: + *indir = SShell::Class::SURF_INSIDE; + *outdir = SShell::Class::SURF_INSIDE; + break; + + case SBspUv::Class::OUTSIDE: + *indir = SShell::Class::SURF_OUTSIDE; + *outdir = SShell::Class::SURF_OUTSIDE; + break; + + case SBspUv::Class::EDGE_PARALLEL: + *indir = SShell::Class::SURF_INSIDE; + *outdir = SShell::Class::SURF_OUTSIDE; + break; + + case SBspUv::Class::EDGE_ANTIPARALLEL: + *indir = SShell::Class::SURF_OUTSIDE; + *outdir = SShell::Class::SURF_INSIDE; + break; + + default: + dbp("TagByClassifiedEdge: fail!"); + *indir = SShell::Class::SURF_OUTSIDE; + *outdir = SShell::Class::SURF_OUTSIDE; + break; + } +} + +static void DEBUGEDGELIST(SEdgeList *sel, SSurface *surf) { + dbp("print %d edges", sel->l.n); + SEdge *se; + for(se = sel->l.First(); se; se = sel->l.NextAfter(se)) { + Vector mid = (se->a).Plus(se->b).ScaledBy(0.5); + Vector arrow = (se->b).Minus(se->a); + swap(arrow.x, arrow.y); + arrow.x *= -1; + arrow = arrow.WithMagnitude(0.01); + arrow = arrow.Plus(mid); + + SS.nakedEdges.AddEdge(surf->PointAt(se->a.x, se->a.y), + surf->PointAt(se->b.x, se->b.y)); + SS.nakedEdges.AddEdge(surf->PointAt(mid.x, mid.y), + surf->PointAt(arrow.x, arrow.y)); + } +} + +//----------------------------------------------------------------------------- +// We are given src, with at least one edge, and avoid, a list of points to +// avoid. We return a chain of edges (that share endpoints), such that no +// point within the avoid list ever occurs in the middle of a chain. And we +// delete the edges in that chain from our source list. +//----------------------------------------------------------------------------- +void SSurface::FindChainAvoiding(SEdgeList *src, SEdgeList *dest, + SPointList *avoid) +{ + ssassert(!src->l.IsEmpty(), "Need at least one edge"); + // Start with an arbitrary edge. + dest->l.Add(src->l.First()); + src->l.ClearTags(); + src->l.First()->tag = 1; + + bool added; + do { + added = false; + // The start and finish of the current edge chain + Vector s = dest->l.First()->a, + f = dest->l.Last()->b; + + // We can attach a new edge at the start or finish, as long as that + // start or finish point isn't in the list of points to avoid. + bool startOkay = !avoid->ContainsPoint(s), + finishOkay = !avoid->ContainsPoint(f); + + // Now look for an unused edge that joins at the start or finish of + // our chain (if permitted by the avoid list). + SEdge *se; + for(se = src->l.First(); se; se = src->l.NextAfter(se)) { + if(se->tag) continue; + if(startOkay && s.Equals(se->b)) { + dest->l.AddToBeginning(se); + s = se->a; + se->tag = 1; + startOkay = !avoid->ContainsPoint(s); + } else if(finishOkay && f.Equals(se->a)) { + dest->l.Add(se); + f = se->b; + se->tag = 1; + finishOkay = !avoid->ContainsPoint(f); + } else { + continue; + } + + added = true; + } + } while(added); + + src->l.RemoveTagged(); +} + +void SSurface::EdgeNormalsWithinSurface(Point2d auv, Point2d buv, + Vector *pt, + Vector *enin, Vector *enout, + Vector *surfn, + uint32_t auxA, + SShell *shell, SShell *sha, SShell *shb) +{ + // the midpoint of the edge + Point2d muv = (auv.Plus(buv)).ScaledBy(0.5); + + *pt = PointAt(muv); + + // If this edge just approximates a curve, then refine our midpoint so + // so that it actually lies on that curve too. Otherwise stuff like + // point-on-face tests will fail, since the point won't actually lie + // on the other face. + hSCurve hc = { auxA }; + SCurve *sc = shell->curve.FindById(hc); + if(sc->isExact && sc->exact.deg != 1) { + double t; + sc->exact.ClosestPointTo(*pt, &t, /*mustConverge=*/false); + *pt = sc->exact.PointAt(t); + } else if(!sc->isExact) { + SSurface *trimmedA = sc->GetSurfaceA(sha, shb), + *trimmedB = sc->GetSurfaceB(sha, shb); + *pt = trimmedA->ClosestPointOnThisAndSurface(trimmedB, *pt); + } + ClosestPointTo(*pt, &muv); + + *surfn = NormalAt(muv.x, muv.y); + + // Compute the edge's inner normal in xyz space. + Vector ab = (PointAt(auv)).Minus(PointAt(buv)), + enxyz = (ab.Cross(*surfn)).WithMagnitude(SS.ChordTolMm()); + // And based on that, compute the edge's inner normal in uv space. This + // vector is perpendicular to the edge in xyz, but not necessarily in uv. + Vector tu, tv, tx, ty; + TangentsAt(muv.x, muv.y, &tu, &tv); + Vector n = tu.Cross(tv); + // since tu and tv may not be orthogonal, use y in place of v, x in place of u. + // |y| = |v|sin(theta) where theta is the angle between tu and tv. + ty = n.Cross(tu).ScaledBy(1.0/tu.MagSquared()); + tx = tv.Cross(n).ScaledBy(1.0/tv.MagSquared()); + + Point2d enuv; + enuv.x = enxyz.Dot(tx) / tx.MagSquared(); + enuv.y = enxyz.Dot(ty) / ty.MagSquared(); + + // Compute the inner and outer normals of this edge (within the srf), + // in xyz space. These are not necessarily antiparallel, if the + // surface is curved. + Vector pin = PointAt(muv.Minus(enuv)), + pout = PointAt(muv.Plus(enuv)); + *enin = pin.Minus(*pt), + *enout = pout.Minus(*pt); +// ideally this should work (fail screwdriver file) +// *enin = enxyz.ScaledBy(-1.0); +// *enout = enxyz; +} + +//----------------------------------------------------------------------------- +// Trim this surface against the specified shell, in the way that's appropriate +// for the specified Boolean operation type (and which operand we are). We +// also need a pointer to the shell that contains our own surface, since that +// contains our original trim curves. +//----------------------------------------------------------------------------- +SSurface SSurface::MakeCopyTrimAgainst(SShell *parent, + SShell *sha, SShell *shb, + SShell *into, + SSurface::CombineAs type, + int dbg_index) +{ + bool opA = (parent == sha); + SShell *agnst = opA ? shb : sha; + + SSurface ret; + // The returned surface is identical, just the trim curves change + ret = *this; + ret.trim = {}; + + // First, build a list of the existing trim curves; update them to use + // the split curves. + STrimBy *stb; + for(stb = trim.First(); stb; stb = trim.NextAfter(stb)) { + STrimBy stn = *stb; + stn.curve = (parent->curve.FindById(stn.curve))->newH; + ret.trim.Add(&stn); + } + + if(type == SSurface::CombineAs::DIFFERENCE && !opA) { + // The second operand of a Boolean difference gets turned inside out + ret.Reverse(); + } + + // Build up our original trim polygon; remember the coordinates could + // be changed if we just flipped the surface normal, and we are using + // the split curves (not the original curves). + SEdgeList orig = {}; + ret.MakeEdgesInto(into, &orig, MakeAs::UV); + ret.trim.Clear(); + // which means that we can't necessarily use the old BSP... + SBspUv *origBsp = SBspUv::From(&orig, &ret); + + // And now intersect the other shell against us + SEdgeList inter = {}; + + SSurface *ss; + for(SCurve &sc : into->curve) { + if(sc.source != SCurve::Source::INTERSECTION) continue; + if(opA) { + if(sc.surfA != h) continue; + ss = shb->surface.FindById(sc.surfB); + } else { + if(sc.surfB != h) continue; + ss = sha->surface.FindById(sc.surfA); + } + int i; + for(i = 1; i < sc.pts.n; i++) { + Vector a = sc.pts[i-1].p, + b = sc.pts[i].p; + + Point2d auv, buv; + ss->ClosestPointTo(a, &(auv.x), &(auv.y)); + ss->ClosestPointTo(b, &(buv.x), &(buv.y)); + + SBspUv::Class c = (ss->bsp) ? ss->bsp->ClassifyEdge(auv, buv, ss) : SBspUv::Class::OUTSIDE; + if(c != SBspUv::Class::OUTSIDE) { + Vector ta = Vector::From(0, 0, 0); + Vector tb = Vector::From(0, 0, 0); + ret.ClosestPointTo(a, &(ta.x), &(ta.y)); + ret.ClosestPointTo(b, &(tb.x), &(tb.y)); + + Vector tn = ret.NormalAt(ta.x, ta.y); + Vector sn = ss->NormalAt(auv.x, auv.y); + + // We are subtracting the portion of our surface that + // lies in the shell, so the in-plane edge normal should + // point opposite to the surface normal. + bool bkwds = true; + if((tn.Cross(b.Minus(a))).Dot(sn) < 0) bkwds = !bkwds; + if((type == SSurface::CombineAs::DIFFERENCE && !opA) || + (type == SSurface::CombineAs::INTERSECTION)) { // Invert all newly created edges for intersection + bkwds = !bkwds; + } + if(bkwds) { + inter.AddEdge(tb, ta, sc.h.v, 1); + } else { + inter.AddEdge(ta, tb, sc.h.v, 0); + } + } + } + } + + // Record all the points where more than two edges join, which I will call + // the choosing points. If two edges join at a non-choosing point, then + // they must either both be kept or both be discarded (since that would + // otherwise create an open contour). + SPointList choosing = {}; + SEdge *se; + for(se = orig.l.First(); se; se = orig.l.NextAfter(se)) { + choosing.IncrementTagFor(se->a); + choosing.IncrementTagFor(se->b); + } + for(se = inter.l.First(); se; se = inter.l.NextAfter(se)) { + choosing.IncrementTagFor(se->a); + choosing.IncrementTagFor(se->b); + } + SPoint *sp; + for(sp = choosing.l.First(); sp; sp = choosing.l.NextAfter(sp)) { + if(sp->tag == 2) { + sp->tag = 1; + } else { + sp->tag = 0; + } + } + choosing.l.RemoveTagged(); + + // The list of edges to trim our new surface, a combination of edges from + // our original and intersecting edge lists. + SEdgeList final = {}; + + while(!orig.l.IsEmpty()) { + SEdgeList chain = {}; + FindChainAvoiding(&orig, &chain, &choosing); + + // Arbitrarily choose an edge within the chain to classify; they + // should all be the same, though. + se = &(chain.l[chain.l.n/2]); + + Point2d auv = (se->a).ProjectXy(), + buv = (se->b).ProjectXy(); + + Vector pt, enin, enout, surfn; + ret.EdgeNormalsWithinSurface(auv, buv, &pt, &enin, &enout, &surfn, + se->auxA, into, sha, shb); + + SShell::Class indir_shell, outdir_shell, indir_orig, outdir_orig; + + indir_orig = SShell::Class::SURF_INSIDE; + outdir_orig = SShell::Class::SURF_OUTSIDE; + + agnst->ClassifyEdge(&indir_shell, &outdir_shell, + ret.PointAt(auv), ret.PointAt(buv), pt, + enin, enout, surfn); + + if(KeepEdge(type, opA, indir_shell, outdir_shell, + indir_orig, outdir_orig)) + { + for(se = chain.l.First(); se; se = chain.l.NextAfter(se)) { + final.AddEdge(se->a, se->b, se->auxA, se->auxB); + } + } + chain.Clear(); + } + + while(!inter.l.IsEmpty()) { + SEdgeList chain = {}; + FindChainAvoiding(&inter, &chain, &choosing); + + // Any edge in the chain, same as above. + se = &(chain.l[chain.l.n/2]); + + Point2d auv = (se->a).ProjectXy(), + buv = (se->b).ProjectXy(); + + Vector pt, enin, enout, surfn; + ret.EdgeNormalsWithinSurface(auv, buv, &pt, &enin, &enout, &surfn, + se->auxA, into, sha, shb); + + SShell::Class indir_shell, outdir_shell, indir_orig, outdir_orig; + + SBspUv::Class c_this = (origBsp) ? origBsp->ClassifyEdge(auv, buv, &ret) : SBspUv::Class::OUTSIDE; + TagByClassifiedEdge(c_this, &indir_orig, &outdir_orig); + + agnst->ClassifyEdge(&indir_shell, &outdir_shell, + ret.PointAt(auv), ret.PointAt(buv), pt, + enin, enout, surfn); + + if(KeepEdge(type, opA, indir_shell, outdir_shell, + indir_orig, outdir_orig)) + { + for(se = chain.l.First(); se; se = chain.l.NextAfter(se)) { + final.AddEdge(se->a, se->b, se->auxA, se->auxB); + } + } + chain.Clear(); + } + + // Cull extraneous edges; duplicates or anti-parallel pairs. In particular, + // we can get duplicate edges if our surface intersects the other shell + // at an edge, so that both surfaces intersect coincident (and both + // generate an intersection edge). + final.CullExtraneousEdges(/*both=*/true); + + // Use our reassembled edges to trim the new surface. + ret.TrimFromEdgeList(&final, /*asUv=*/true); + + SPolygon poly = {}; + final.l.ClearTags(); + if(!final.AssemblePolygon(&poly, NULL, /*keepDir=*/true)) +#pragma omp critical + { + into->booleanFailed = true; + dbp("failed: I=%d, avoid=%d", I+dbg_index, choosing.l.n); + DEBUGEDGELIST(&final, &ret); + } + poly.Clear(); + + choosing.Clear(); + final.Clear(); + inter.Clear(); + orig.Clear(); + return ret; +} + +void SShell::CopySurfacesTrimAgainst(SShell *sha, SShell *shb, SShell *into, SSurface::CombineAs type) { + std::vector ssn(surface.n); +#pragma omp parallel for + for (int i = 0; i < surface.n; i++) + { + SSurface *ss = &surface[i]; + ssn[i] = ss->MakeCopyTrimAgainst(this, sha, shb, into, type, i); + } + + for (int i = 0; i < surface.n; i++) + { + surface[i].newH = into->surface.AddAndAssignId(&ssn[i]); + } + I += surface.n; +} + +void SShell::MakeIntersectionCurvesAgainst(SShell *agnst, SShell *into) { +#pragma omp parallel for + for(int i = 0; i< surface.n; i++) { + SSurface *sa = &surface[i]; + + for(SSurface &sb : agnst->surface){ + // Intersect every surface from our shell against every surface + // from agnst; this will add zero or more curves to the curve + // list for into. + sa->IntersectAgainst(&sb, this, agnst, into); + } + } +} + +void SShell::CleanupAfterBoolean() { + for(SSurface &ss : surface) { + ss.edges.Clear(); + } +} + +//----------------------------------------------------------------------------- +// All curves contain handles to the two surfaces that they trim. After a +// Boolean or assembly, we must rewrite those handles to refer to the curves +// by their new IDs. +//----------------------------------------------------------------------------- +void SShell::RewriteSurfaceHandlesForCurves(SShell *a, SShell *b) { + for(SCurve &sc : curve) { + sc.surfA = sc.GetSurfaceA(a, b)->newH, + sc.surfB = sc.GetSurfaceB(a, b)->newH; + } +} + +//----------------------------------------------------------------------------- +// Copy all the surfaces and curves from two different shells into a single +// shell. The only difficulty is to rewrite all of their handles; we don't +// look for any surface intersections, so if two objects interfere then the +// result is just self-intersecting. This is used for assembly, since it's +// much faster than merging as union. +//----------------------------------------------------------------------------- +void SShell::MakeFromAssemblyOf(SShell *a, SShell *b) { + booleanFailed = false; + + Vector t = Vector::From(0, 0, 0); + Quaternion q = Quaternion::IDENTITY; + int i = 0; + SShell *ab; + + // First, copy over all the curves. Note which shell (a or b) each curve + // came from, but assign it a new ID. + curve.ReserveMore(a->curve.n + b->curve.n); + SCurve cn; + for(i = 0; i < 2; i++) { + ab = (i == 0) ? a : b; + for(SCurve &c : ab->curve) { + cn = SCurve::FromTransformationOf(&c, t, q, 1.0); + cn.source = (i == 0) ? SCurve::Source::A : SCurve::Source::B; + // surfA and surfB are wrong now, and we can't fix them until + // we've assigned IDs to the surfaces. So we'll get that later. + c.newH = curve.AddAndAssignId(&cn); + } + } + + // Likewise copy over all the surfaces. + surface.ReserveMore(a->surface.n + b->surface.n); + SSurface sn; + for(i = 0; i < 2; i++) { + ab = (i == 0) ? a : b; + for(SSurface &s : ab->surface) { + sn = SSurface::FromTransformationOf(&s, t, q, 1.0, /*includingTrims=*/true); + // All the trim curve IDs get rewritten; we know the new handles + // to the curves since we recorded them in the previous step. + STrimBy *stb; + for(stb = sn.trim.First(); stb; stb = sn.trim.NextAfter(stb)) { + stb->curve = ab->curve.FindById(stb->curve)->newH; + } + s.newH = surface.AddAndAssignId(&sn); + } + } + + // Finally, rewrite the surfaces associated with each curve to use the + // new handles. + RewriteSurfaceHandlesForCurves(a, b); +} + +void SShell::MakeFromBoolean(SShell *a, SShell *b, SSurface::CombineAs type) { + booleanFailed = false; + + a->MakeClassifyingBsps(NULL); + b->MakeClassifyingBsps(NULL); + + // Copy over all the original curves, splitting them so that a + // piecewise linear segment never crosses a surface from the other + // shell. + a->CopyCurvesSplitAgainst(/*opA=*/true, b, this); + b->CopyCurvesSplitAgainst(/*opA=*/false, a, this); + + // Generate the intersection curves for each surface in A against all + // the surfaces in B (which is all of the intersection curves). + a->MakeIntersectionCurvesAgainst(b, this); + + for(SCurve &sc : curve) { + SSurface *srfA = sc.GetSurfaceA(a, b), + *srfB = sc.GetSurfaceB(a, b); + + sc.RemoveShortSegments(srfA, srfB); + } + + // And clean up the piecewise linear things we made as a calculation aid + a->CleanupAfterBoolean(); + b->CleanupAfterBoolean(); + // Remake the classifying BSPs with the split (and short-segment-removed) + // curves + a->MakeClassifyingBsps(this); + b->MakeClassifyingBsps(this); + + if(b->surface.IsEmpty() || a->surface.IsEmpty()) { + I = 1000000; + } else { + I = 0; + } + // Then trim and copy the surfaces + a->CopySurfacesTrimAgainst(a, b, this, type); + b->CopySurfacesTrimAgainst(a, b, this, type); + + // Now that we've copied the surfaces, we know their new hSurfaces, so + // rewrite the curves to refer to the surfaces by their handles in the + // result. + RewriteSurfaceHandlesForCurves(a, b); + + // And clean up the piecewise linear things we made as a calculation aid + a->CleanupAfterBoolean(); + b->CleanupAfterBoolean(); +} + +//----------------------------------------------------------------------------- +// All of the BSP routines that we use to perform and accelerate polygon ops. +//----------------------------------------------------------------------------- +void SShell::MakeClassifyingBsps(SShell *useCurvesFrom) { +#pragma omp parallel for + for(int i = 0; il.First(); se; se = el->l.NextAfter(se)) { + work.AddEdge(se->a, se->b, se->auxA, se->auxB); + } + std::sort(work.l.begin(), work.l.end(), [](SEdge const &a, SEdge const &b) { + double la = (a.a).Minus(a.b).Magnitude(), lb = (b.a).Minus(b.b).Magnitude(); + // Sort in descending order, longest first. This improves numerical + // stability for the normals. + return la > lb; + }); + SBspUv *bsp = NULL; + for(se = work.l.First(); se; se = work.l.NextAfter(se)) { + bsp = InsertOrCreateEdge(bsp, (se->a).ProjectXy(), (se->b).ProjectXy(), srf); + } + + work.Clear(); + return bsp; +} + +//----------------------------------------------------------------------------- +// The points in this BSP are in uv space, but we want to apply our tolerances +// consistently in xyz (i.e., we want to say a point is on-edge if its xyz +// distance to that edge is less than LENGTH_EPS, irrespective of its distance +// in uv). So we linearize the surface about the point we're considering and +// then do the test. That preserves point-on-line relationships, and the only +// time we care about exact correctness is when we're very close to the line, +// which is when the linearization is accurate. +//----------------------------------------------------------------------------- + +void SBspUv::ScalePoints(Point2d *pt, Point2d *a, Point2d *b, SSurface *srf) const { + Vector tu, tv; + srf->TangentsAt(pt->x, pt->y, &tu, &tv); + double mu = tu.Magnitude(), mv = tv.Magnitude(); + + pt->x *= mu; pt->y *= mv; + a ->x *= mu; a ->y *= mv; + b ->x *= mu; b ->y *= mv; +} + +double SBspUv::ScaledSignedDistanceToLine(Point2d pt, Point2d a, Point2d b, + SSurface *srf) const +{ + ScalePoints(&pt, &a, &b, srf); + + Point2d n = ((b.Minus(a)).Normal()).WithMagnitude(1); + double d = a.Dot(n); + + return pt.Dot(n) - d; +} + +double SBspUv::ScaledDistanceToLine(Point2d pt, Point2d a, Point2d b, bool asSegment, + SSurface *srf) const +{ + ScalePoints(&pt, &a, &b, srf); + + return pt.DistanceToLine(a, b, asSegment); +} + +SBspUv *SBspUv::InsertOrCreateEdge(SBspUv *where, Point2d ea, Point2d eb, SSurface *srf) { + if(where == NULL) { + SBspUv *ret = Alloc(); + ret->a = ea; + ret->b = eb; + return ret; + } + where->InsertEdge(ea, eb, srf); + return where; +} + +void SBspUv::InsertEdge(Point2d ea, Point2d eb, SSurface *srf) { + double dea = ScaledSignedDistanceToLine(ea, a, b, srf), + deb = ScaledSignedDistanceToLine(eb, a, b, srf); + + if(fabs(dea) < LENGTH_EPS && fabs(deb) < LENGTH_EPS) { + // Line segment is coincident with this one, store in same node + SBspUv *m = Alloc(); + m->a = ea; + m->b = eb; + m->more = more; + more = m; + } else if(fabs(dea) < LENGTH_EPS) { + // Point A lies on this line, but point B does not + if(deb > 0) { + pos = InsertOrCreateEdge(pos, ea, eb, srf); + } else { + neg = InsertOrCreateEdge(neg, ea, eb, srf); + } + } else if(fabs(deb) < LENGTH_EPS) { + // Point B lies on this line, but point A does not + if(dea > 0) { + pos = InsertOrCreateEdge(pos, ea, eb, srf); + } else { + neg = InsertOrCreateEdge(neg, ea, eb, srf); + } + } else if(dea > 0 && deb > 0) { + pos = InsertOrCreateEdge(pos, ea, eb, srf); + } else if(dea < 0 && deb < 0) { + neg = InsertOrCreateEdge(neg, ea, eb, srf); + } else { + // New edge crosses this one; we need to split. + Point2d n = ((b.Minus(a)).Normal()).WithMagnitude(1); + double d = a.Dot(n); + double t = (d - n.Dot(ea)) / (n.Dot(eb.Minus(ea))); + Point2d pi = ea.Plus((eb.Minus(ea)).ScaledBy(t)); + if(dea > 0) { + pos = InsertOrCreateEdge(pos, ea, pi, srf); + neg = InsertOrCreateEdge(neg, pi, eb, srf); + } else { + neg = InsertOrCreateEdge(neg, ea, pi, srf); + pos = InsertOrCreateEdge(pos, pi, eb, srf); + } + } + return; +} + +SBspUv::Class SBspUv::ClassifyPoint(Point2d p, Point2d eb, SSurface *srf) const { + double dp = ScaledSignedDistanceToLine(p, a, b, srf); + + if(fabs(dp) < LENGTH_EPS) { + const SBspUv *f = this; + while(f) { + Point2d ba = (f->b).Minus(f->a); + if(ScaledDistanceToLine(p, f->a, ba, /*asSegment=*/true, srf) < LENGTH_EPS) { + if(ScaledDistanceToLine(eb, f->a, ba, /*asSegment=*/false, srf) < LENGTH_EPS){ + if(ba.Dot(eb.Minus(p)) > 0) { + return Class::EDGE_PARALLEL; + } else { + return Class::EDGE_ANTIPARALLEL; + } + } else { + return Class::EDGE_OTHER; + } + } + f = f->more; + } + // Pick arbitrarily which side to send it down, doesn't matter + Class c1 = neg ? neg->ClassifyPoint(p, eb, srf) : Class::OUTSIDE; + Class c2 = pos ? pos->ClassifyPoint(p, eb, srf) : Class::INSIDE; + if(c1 != c2) { + dbp("MISMATCH: %d %d %08x %08x", c1, c2, neg, pos); + } + return c1; + } else if(dp > 0) { + return pos ? pos->ClassifyPoint(p, eb, srf) : Class::INSIDE; + } else { + return neg ? neg->ClassifyPoint(p, eb, srf) : Class::OUTSIDE; + } +} + +SBspUv::Class SBspUv::ClassifyEdge(Point2d ea, Point2d eb, SSurface *srf) const { + SBspUv::Class ret = ClassifyPoint((ea.Plus(eb)).ScaledBy(0.5), eb, srf); + if(ret == Class::EDGE_OTHER) { + // Perhaps the edge is tangent at its midpoint (and we screwed up + // somewhere earlier and failed to split it); try a different + // point on the edge. + ret = ClassifyPoint(ea.Plus((eb.Minus(ea)).ScaledBy(0.294)), eb, srf); + } + return ret; +} + +double SBspUv::MinimumDistanceToEdge(Point2d p, SSurface *srf) const { + + double dn = (neg) ? neg->MinimumDistanceToEdge(p, srf) : VERY_POSITIVE; + double dp = (pos) ? pos->MinimumDistanceToEdge(p, srf) : VERY_POSITIVE; + + Point2d as = a, bs = b; + ScalePoints(&p, &as, &bs, srf); + double d = p.DistanceToLine(as, bs.Minus(as), /*asSegment=*/true); + + return min(d, min(dn, dp)); +} + +} // namespace SolveSpace diff --git a/src/srf/curve.cpp b/src/srf/curve.cpp new file mode 100644 index 0000000..ef3e19a --- /dev/null +++ b/src/srf/curve.cpp @@ -0,0 +1,886 @@ +//----------------------------------------------------------------------------- +// Anything involving curves and sets of curves (except for the real math, +// which is in ratpoly.cpp). +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +SBezier SBezier::From(Vector4 p0, Vector4 p1) { + SBezier ret = {}; + ret.deg = 1; + ret.weight[0] = p0.w; + ret.ctrl [0] = p0.PerspectiveProject(); + ret.weight[1] = p1.w; + ret.ctrl [1] = p1.PerspectiveProject(); + return ret; +} + +SBezier SBezier::From(Vector4 p0, Vector4 p1, Vector4 p2) { + SBezier ret = {}; + ret.deg = 2; + ret.weight[0] = p0.w; + ret.ctrl [0] = p0.PerspectiveProject(); + ret.weight[1] = p1.w; + ret.ctrl [1] = p1.PerspectiveProject(); + ret.weight[2] = p2.w; + ret.ctrl [2] = p2.PerspectiveProject(); + return ret; +} + +SBezier SBezier::From(Vector4 p0, Vector4 p1, Vector4 p2, Vector4 p3) { + SBezier ret = {}; + ret.deg = 3; + ret.weight[0] = p0.w; + ret.ctrl [0] = p0.PerspectiveProject(); + ret.weight[1] = p1.w; + ret.ctrl [1] = p1.PerspectiveProject(); + ret.weight[2] = p2.w; + ret.ctrl [2] = p2.PerspectiveProject(); + ret.weight[3] = p3.w; + ret.ctrl [3] = p3.PerspectiveProject(); + return ret; +} + +SBezier SBezier::From(Vector p0, Vector p1) { + return SBezier::From(p0.Project4d(), + p1.Project4d()); +} + +SBezier SBezier::From(Vector p0, Vector p1, Vector p2) { + return SBezier::From(p0.Project4d(), + p1.Project4d(), + p2.Project4d()); +} + +SBezier SBezier::From(Vector p0, Vector p1, Vector p2, Vector p3) { + return SBezier::From(p0.Project4d(), + p1.Project4d(), + p2.Project4d(), + p3.Project4d()); +} + +Vector SBezier::Start() const { + return ctrl[0]; +} + +Vector SBezier::Finish() const { + return ctrl[deg]; +} + +void SBezier::Reverse() { + int i; + for(i = 0; i < (deg+1)/2; i++) { + swap(ctrl[i], ctrl[deg-i]); + swap(weight[i], weight[deg-i]); + } +} + +void SBezier::ScaleSelfBy(double s) { + int i; + for(i = 0; i <= deg; i++) { + ctrl[i] = ctrl[i].ScaledBy(s); + } +} + +void SBezier::GetBoundingProjd(Vector u, Vector orig, + double *umin, double *umax) const +{ + int i; + for(i = 0; i <= deg; i++) { + double ut = ((ctrl[i]).Minus(orig)).Dot(u); + if(ut < *umin) *umin = ut; + if(ut > *umax) *umax = ut; + } +} + +SBezier SBezier::TransformedBy(Vector t, Quaternion q, double scale) const { + SBezier ret = *this; + int i; + for(i = 0; i <= deg; i++) { + ret.ctrl[i] = (ret.ctrl[i]).ScaledBy(scale); + ret.ctrl[i] = (q.Rotate(ret.ctrl[i])).Plus(t); + } + return ret; +} + +//----------------------------------------------------------------------------- +// Does this curve lie entirely within the specified plane? It does if all +// the control points lie in that plane. +//----------------------------------------------------------------------------- +bool SBezier::IsInPlane(Vector n, double d) const { + int i; + for(i = 0; i <= deg; i++) { + if(fabs((ctrl[i]).Dot(n) - d) > LENGTH_EPS) { + return false; + } + } + return true; +} + +//----------------------------------------------------------------------------- +// Is this Bezier exactly the arc of a circle, projected along the specified +// axis? If yes, return that circle's center and radius. +//----------------------------------------------------------------------------- +bool SBezier::IsCircle(Vector axis, Vector *center, double *r) const { + if(deg != 2) return false; + + if(ctrl[1].DistanceToLine(ctrl[0], ctrl[2].Minus(ctrl[0])) < LENGTH_EPS) { + // This is almost a line segment. So it's a circle with very large + // radius, which is likely to make code that tries to handle circles + // blow up. So return false. + return false; + } + + Vector t0 = (ctrl[0]).Minus(ctrl[1]), + t2 = (ctrl[2]).Minus(ctrl[1]), + r0 = axis.Cross(t0), + r2 = axis.Cross(t2); + + *center = Vector::AtIntersectionOfLines(ctrl[0], (ctrl[0]).Plus(r0), + ctrl[2], (ctrl[2]).Plus(r2), + NULL, NULL, NULL); + + double rd0 = center->Minus(ctrl[0]).Magnitude(), + rd2 = center->Minus(ctrl[2]).Magnitude(); + if(fabs(rd0 - rd2) > LENGTH_EPS) { + return false; + } + *r = rd0; + + Vector u = r0.WithMagnitude(1), + v = (axis.Cross(u)).WithMagnitude(1); + Point2d c2 = center->Project2d(u, v), + pa2 = (ctrl[0]).Project2d(u, v).Minus(c2), + pb2 = (ctrl[2]).Project2d(u, v).Minus(c2); + + double thetaa = atan2(pa2.y, pa2.x), // in fact always zero due to csys + thetab = atan2(pb2.y, pb2.x), + dtheta = WRAP_NOT_0(thetab - thetaa, 2*PI); + if(dtheta > PI) { + // Not possible with a second order Bezier arc; so we must have + // the points backwards. + dtheta = 2*PI - dtheta; + } + + if(fabs(weight[1] - cos(dtheta/2)) > LENGTH_EPS) { + return false; + } + + return true; +} + +bool SBezier::IsRational() const { + int i; + for(i = 0; i <= deg; i++) { + if(fabs(weight[i] - 1) > LENGTH_EPS) return true; + } + return false; +} + +//----------------------------------------------------------------------------- +// Apply a perspective transformation to a rational Bezier curve, calculating +// the new weights as required. +//----------------------------------------------------------------------------- +SBezier SBezier::InPerspective(Vector u, Vector v, Vector n, + Vector origin, double cameraTan) const +{ + Quaternion q = Quaternion::From(u, v); + q = q.Inverse(); + // we want Q*(p - o) = Q*p - Q*o + SBezier ret = this->TransformedBy(q.Rotate(origin).ScaledBy(-1), q, 1.0); + int i; + for(i = 0; i <= deg; i++) { + Vector4 ct = Vector4::From(ret.weight[i], ret.ctrl[i]); + // so the desired curve, before perspective, is + // (x/w, y/w, z/w) + // and after perspective is + // ((x/w)/(1 - (z/w)*cameraTan, ... + // = (x/(w - z*cameraTan), ... + // so we want to let w' = w - z*cameraTan + ct.w = ct.w - ct.z*cameraTan; + + ret.ctrl[i] = ct.PerspectiveProject(); + ret.weight[i] = ct.w; + } + return ret; +} + +bool SBezier::Equals(SBezier *b) const { + // We just test of identical degree and control points, even though two + // curves could still be coincident (even sharing endpoints). + if(deg != b->deg) return false; + int i; + for(i = 0; i <= deg; i++) { + if(!(ctrl[i]).Equals(b->ctrl[i])) return false; + if(fabs(weight[i] - b->weight[i]) > LENGTH_EPS) return false; + } + return true; +} + +void SBezierList::Clear() { + l.Clear(); +} + +void SBezierList::ScaleSelfBy(double s) { + SBezier *sb; + for(sb = l.First(); sb; sb = l.NextAfter(sb)) { + sb->ScaleSelfBy(s); + } +} + +//----------------------------------------------------------------------------- +// If our list contains multiple identical Beziers (in either forward or +// reverse order), then cull them. If both is true, both beziers are removed. +// Otherwise only one of them is removed. +//----------------------------------------------------------------------------- +void SBezierList::CullIdenticalBeziers(bool both) { + int i, j; + + l.ClearTags(); + for(i = 0; i < l.n; i++) { + SBezier *bi = &(l[i]), bir; + bir = *bi; + bir.Reverse(); + + for(j = i + 1; j < l.n; j++) { + SBezier *bj = &(l[j]); + if(bj->Equals(bi) || + bj->Equals(&bir)) + { + if (both) bi->tag = 1; + bj->tag = 1; + } + } + } + l.RemoveTagged(); +} + +//----------------------------------------------------------------------------- +// Find all the points where a list of Bezier curves intersects another list +// of Bezier curves. We do this by intersecting their piecewise linearizations, +// and then refining any intersections that we find to lie exactly on the +// curves. So this will screw up on tangencies and stuff, but otherwise should +// be fine. +//----------------------------------------------------------------------------- +void SBezierList::AllIntersectionsWith(SBezierList *sblb, SPointList *spl) const { + for(const SBezier *sba = l.First(); sba; sba = l.NextAfter(sba)) { + for(const SBezier *sbb = sblb->l.First(); sbb; sbb = sblb->l.NextAfter(sbb)) { + sbb->AllIntersectionsWith(sba, spl); + } + } +} +void SBezier::AllIntersectionsWith(const SBezier *sbb, SPointList *spl) const { + SPointList splRaw = {}; + SEdgeList sea, seb; + sea = {}; + seb = {}; + this->MakePwlInto(&sea); + sbb ->MakePwlInto(&seb); + SEdge *se; + for(se = sea.l.First(); se; se = sea.l.NextAfter(se)) { + // This isn't quite correct, since AnyEdgeCrossings doesn't count + // the case where two pairs of line segments intersect at their + // vertices. So this isn't robust, although that case isn't very + // likely. + seb.AnyEdgeCrossings(se->a, se->b, NULL, &splRaw); + } + SPoint *sp; + for(sp = splRaw.l.First(); sp; sp = splRaw.l.NextAfter(sp)) { + Vector p = sp->p; + if(PointOnThisAndCurve(sbb, &p)) { + if(!spl->ContainsPoint(p)) spl->Add(p); + } + } + sea.Clear(); + seb.Clear(); + splRaw.Clear(); +} + +//----------------------------------------------------------------------------- +// Find a plane that contains all of the curves in this list. If the curves +// are all colinear (or coincident, or empty), then that plane is not exactly +// determined but we choose the additional degree(s) of freedom arbitrarily. +// Returns true if all the curves are coplanar, otherwise false. +//----------------------------------------------------------------------------- +bool SBezierList::GetPlaneContainingBeziers(Vector *p, Vector *u, Vector *v, + Vector *notCoplanarAt) const +{ + Vector pt, ptFar, ptOffLine, dp, n; + double farMax, offLineMax; + int i; + + // Get any point on any Bezier; or an arbitrary point if list is empty. + if(!l.IsEmpty()) { + pt = l[0].Start(); + } else { + pt = Vector::From(0, 0, 0); + } + ptFar = ptOffLine = pt; + + // Get the point farthest from our arbitrary point. + farMax = VERY_NEGATIVE; + for(const SBezier *sb = l.First(); sb; sb = l.NextAfter(sb)) { + for(i = 0; i <= sb->deg; i++) { + double m = (pt.Minus(sb->ctrl[i])).Magnitude(); + if(m > farMax) { + ptFar = sb->ctrl[i]; + farMax = m; + } + } + } + if(ptFar.Equals(pt)) { + // The points are all coincident. So neither basis vector matters. + *p = pt; + *u = Vector::From(1, 0, 0); + *v = Vector::From(0, 1, 0); + return true; + } + + // Get the point farthest from the line between pt and ptFar + dp = ptFar.Minus(pt); + offLineMax = VERY_NEGATIVE; + for(const SBezier *sb = l.First(); sb; sb = l.NextAfter(sb)) { + for(i = 0; i <= sb->deg; i++) { + double m = (sb->ctrl[i]).DistanceToLine(pt, dp); + if(m > offLineMax) { + ptOffLine = sb->ctrl[i]; + offLineMax = m; + } + } + } + + *p = pt; + if(offLineMax < LENGTH_EPS) { + // The points are all colinear; so choose the second basis vector + // arbitrarily. + *u = (ptFar.Minus(pt)).WithMagnitude(1); + *v = (u->Normal(0)).WithMagnitude(1); + } else { + // The points actually define a plane. + n = (ptFar.Minus(pt)).Cross(ptOffLine.Minus(pt)); + *u = (n.Normal(0)).WithMagnitude(1); + *v = (n.Normal(1)).WithMagnitude(1); + } + + // So we have a plane; but check whether all of the points lie in that + // plane. + n = u->Cross(*v); + n = n.WithMagnitude(1); + double d = p->Dot(n); + for(const SBezier *sb = l.First(); sb; sb = l.NextAfter(sb)) { + for(i = 0; i <= sb->deg; i++) { + if(fabs(n.Dot(sb->ctrl[i]) - d) > LENGTH_EPS) { + if(notCoplanarAt) *notCoplanarAt = sb->ctrl[i]; + return false; + } + } + } + return true; +} + +//----------------------------------------------------------------------------- +// Assemble curves in sbl into a single loop. The curves may appear in any +// direction (start to finish, or finish to start), and will be reversed if +// necessary. The curves in the returned loop are removed from sbl, even if +// the loop cannot be closed. +//----------------------------------------------------------------------------- +SBezierLoop SBezierLoop::FromCurves(SBezierList *sbl, + bool *allClosed, SEdge *errorAt) +{ + SBezierLoop loop = {}; + + if(sbl->l.n < 1) { + *allClosed = false; + return loop; + } + sbl->l.ClearTags(); + + SBezier *first = &(sbl->l[0]); + first->tag = 1; + loop.l.Add(first); + Vector start = first->Start(); + Vector hanging = first->Finish(); + int auxA = first->auxA; + + sbl->l.RemoveTagged(); + + while(!sbl->l.IsEmpty() && !hanging.Equals(start)) { + int i; + bool foundNext = false; + for(i = 0; i < sbl->l.n; i++) { + SBezier *test = &(sbl->l[i]); + + if((test->Finish()).Equals(hanging) && test->auxA == auxA) { + test->Reverse(); + // and let the next test catch it + } + if((test->Start()).Equals(hanging) && test->auxA == auxA) { + test->tag = 1; + loop.l.Add(test); + hanging = test->Finish(); + sbl->l.RemoveTagged(); + foundNext = true; + break; + } + } + if(!foundNext) { + // The loop completed without finding the hanging edge, so + // it's an open loop + errorAt->a = hanging; + errorAt->b = start; + *allClosed = false; + return loop; + } + } + if(hanging.Equals(start)) { + *allClosed = true; + } else { + // We ran out of edges without forming a closed loop. + errorAt->a = hanging; + errorAt->b = start; + *allClosed = false; + } + + return loop; +} + +void SBezierLoop::Reverse() { + l.Reverse(); + SBezier *sb; + for(sb = l.First(); sb; sb = l.NextAfter(sb)) { + // If we didn't reverse each curve, then the next curve in list would + // share your start, not your finish. + sb->Reverse(); + } +} + +void SBezierLoop::GetBoundingProjd(Vector u, Vector orig, + double *umin, double *umax) const +{ + for(const SBezier *sb = l.First(); sb; sb = l.NextAfter(sb)) { + sb->GetBoundingProjd(u, orig, umin, umax); + } +} + +void SBezierLoop::MakePwlInto(SContour *sc, double chordTol) const { + for(const SBezier *sb = l.First(); sb; sb = l.NextAfter(sb)) { + sb->MakePwlInto(sc, chordTol); + // Avoid double points at join between Beziers; except that + // first and last points should be identical. + if(l.NextAfter(sb) != NULL) { + sc->l.RemoveLast(1); + } + } + // Ensure that it's exactly closed, not just within a numerical tolerance. + if((sc->l.Last()->p).Equals(sc->l.First()->p)) { + *sc->l.Last() = *sc->l.First(); + } +} + +bool SBezierLoop::IsClosed() const { + if(l.IsEmpty()) return false; + Vector s = l.First()->Start(), + f = l.Last()->Finish(); + return s.Equals(f); +} + + +//----------------------------------------------------------------------------- +// Assemble the curves in sbl into multiple loops, and piecewise linearize the +// curves into poly. If we can't close a contour, then we add it to +// openContours (if that isn't NULL) and keep going; so this works even if the +// input contains a mix of open and closed curves. +//----------------------------------------------------------------------------- +SBezierLoopSet SBezierLoopSet::From(SBezierList *sbl, SPolygon *poly, + double chordTol, + bool *allClosed, SEdge *errorAt, + SBezierLoopSet *openContours) +{ + SBezierLoopSet ret = {}; + + *allClosed = true; + while(!sbl->l.IsEmpty()) { + bool thisClosed; + SBezierLoop loop; + loop = SBezierLoop::FromCurves(sbl, &thisClosed, errorAt); + if(!thisClosed) { + // Record open loops in a separate list, if requested. + *allClosed = false; + if(openContours) { + openContours->l.Add(&loop); + } else { + loop.Clear(); + } + } else { + ret.l.Add(&loop); + poly->AddEmptyContour(); + loop.MakePwlInto(poly->l.Last(), chordTol); + } + } + + poly->normal = poly->ComputeNormal(); + ret.normal = poly->normal; + if(poly->l.n > 0) { + ret.point = poly->AnyPoint(); + } else { + ret.point = Vector::From(0, 0, 0); + } + + return ret; +} + +void SBezierLoopSet::GetBoundingProjd(Vector u, Vector orig, + double *umin, double *umax) const +{ + for(const SBezierLoop *sbl = l.First(); sbl; sbl = l.NextAfter(sbl)) { + sbl->GetBoundingProjd(u, orig, umin, umax); + } +} + +double SBezierLoopSet::SignedArea() { + if(EXACT(area == 0.0)) { + SPolygon sp = {}; + MakePwlInto(&sp); + sp.normal = sp.ComputeNormal(); + area = sp.SignedArea(); + sp.Clear(); + } + return area; +} + +//----------------------------------------------------------------------------- +// Convert all the Beziers into piecewise linear form, and assemble that into +// a polygon, one contour per loop. +//----------------------------------------------------------------------------- +void SBezierLoopSet::MakePwlInto(SPolygon *sp) const { + for(const SBezierLoop *sbl = l.First(); sbl; sbl = l.NextAfter(sbl)) { + sp->AddEmptyContour(); + sbl->MakePwlInto(sp->l.Last()); + } +} + +void SBezierLoopSet::Clear() { + int i; + for(i = 0; i < l.n; i++) { + (l[i]).Clear(); + } + l.Clear(); +} + +//----------------------------------------------------------------------------- +// An export helper function. We start with a list of Bezier curves, and +// assemble them into loops. We find the outer loops, and find the outer loops' +// inner loops, and group them accordingly. +//----------------------------------------------------------------------------- +void SBezierLoopSetSet::FindOuterFacesFrom(SBezierList *sbl, SPolygon *spxyz, + SSurface *srfuv, + double chordTol, + bool *allClosed, SEdge *notClosedAt, + bool *allCoplanar, Vector *notCoplanarAt, + SBezierLoopSet *openContours) +{ + SSurface srfPlane; + if(!srfuv) { + Vector p, u, v; + *allCoplanar = + sbl->GetPlaneContainingBeziers(&p, &u, &v, notCoplanarAt); + if(!*allCoplanar) { + // Don't even try to assemble them into loops if they're not + // all coplanar. + if(openContours) { + SBezier *sb; + for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) { + SBezierLoop sbl={}; + sbl.l.Add(sb); + openContours->l.Add(&sbl); + } + } + return; + } + // All the curves lie in a plane through p with basis vectors u and v. + srfPlane = SSurface::FromPlane(p, u, v); + srfuv = &srfPlane; + } + + int i, j; + // Assemble the Bezier trim curves into closed loops; we also get the + // piecewise linearization of the curves (in the SPolygon spxyz), as a + // calculation aid for the loop direction. + SBezierLoopSet sbls = SBezierLoopSet::From(sbl, spxyz, chordTol, + allClosed, notClosedAt, + openContours); + if(sbls.l.n != spxyz->l.n) return; + + // Convert the xyz piecewise linear to uv piecewise linear. + SPolygon spuv = {}; + SContour *sc; + for(sc = spxyz->l.First(); sc; sc = spxyz->l.NextAfter(sc)) { + spuv.AddEmptyContour(); + SPoint *pt; + for(pt = sc->l.First(); pt; pt = sc->l.NextAfter(pt)) { + double u, v; + srfuv->ClosestPointTo(pt->p, &u, &v); + spuv.l.Last()->AddPoint(Vector::From(u, v, 0)); + } + } + spuv.normal = Vector::From(0, 0, 1); // must be, since it's in xy plane now + + static const int OUTER_LOOP = 10; + static const int INNER_LOOP = 20; + static const int USED_LOOP = 30; + // Fix the contour directions; we do this properly, in uv space, so it + // works for curved surfaces too (important for STEP export). + spuv.FixContourDirections(); + for(i = 0; i < spuv.l.n; i++) { + SContour *contour = &(spuv.l[i]); + SBezierLoop *bl = &(sbls.l[i]); + if(contour->tag) { + // This contour got reversed in the polygon to make the directions + // consistent, so the same must be necessary for the Bezier loop. + bl->Reverse(); + } + if(contour->IsClockwiseProjdToNormal(spuv.normal)) { + bl->tag = INNER_LOOP; + } else { + bl->tag = OUTER_LOOP; + } + } + + bool loopsRemaining = true; + while(loopsRemaining) { + loopsRemaining = false; + for(i = 0; i < sbls.l.n; i++) { + SBezierLoop *loop = &(sbls.l[i]); + if(loop->tag != OUTER_LOOP) continue; + + // Check if this contour contains any outer loops; if it does, then + // we should do those "inner outer loops" first; otherwise we + // will steal their holes, since their holes also lie inside this + // contour. + for(j = 0; j < sbls.l.n; j++) { + SBezierLoop *outer = &(sbls.l[j]); + if(i == j) continue; + if(outer->tag != OUTER_LOOP) continue; + + Vector p = spuv.l[j].AnyEdgeMidpoint(); + if(spuv.l[i].ContainsPointProjdToNormal(spuv.normal, p)) { + break; + } + } + if(j < sbls.l.n) { + // It does, can't do this one yet. + continue; + } + + SBezierLoopSet outerAndInners = {}; + loopsRemaining = true; + loop->tag = USED_LOOP; + outerAndInners.l.Add(loop); + int auxA = 0; + if(loop->l.n > 0) auxA = loop->l[0].auxA; + + for(j = 0; j < sbls.l.n; j++) { + SBezierLoop *inner = &(sbls.l[j]); + if(inner->tag != INNER_LOOP) continue; + if(inner->l.n < 1) continue; + if(inner->l[0].auxA != auxA) continue; + + Vector p = spuv.l[j].AnyEdgeMidpoint(); + if(spuv.l[i].ContainsPointProjdToNormal(spuv.normal, p)) { + outerAndInners.l.Add(inner); + inner->tag = USED_LOOP; + } + } + + outerAndInners.point = srfuv->PointAt(0, 0); + outerAndInners.normal = srfuv->NormalAt(0, 0); + l.Add(&outerAndInners); + } + } + + // If we have poorly-formed loops--for example, overlapping zero-area + // stuff--then we can end up with leftovers. We use this function to + // group stuff into closed paths for export when possible, so it's bad + // to screw up on that stuff. So just add them onto the open curve list. + // Very ugly, but better than losing curves. + for(i = 0; i < sbls.l.n; i++) { + SBezierLoop *loop = &(sbls.l[i]); + if(loop->tag == USED_LOOP) continue; + + if(openContours) { + openContours->l.Add(loop); + } else { + loop->Clear(); + } + // but don't free the used loops, since we shallow-copied them to + // ourself + } + + sbls.l.Clear(); // not sbls.Clear(), since that would deep-clear + spuv.Clear(); +} + +void SBezierLoopSetSet::AddOpenPath(SBezier *sb) { + SBezierLoop sbl = {}; + sbl.l.Add(sb); + + SBezierLoopSet sbls = {}; + sbls.l.Add(&sbl); + + l.Add(&sbls); +} + +void SBezierLoopSetSet::Clear() { + SBezierLoopSet *sbls; + for(sbls = l.First(); sbls; sbls = l.NextAfter(sbls)) { + sbls->Clear(); + } + l.Clear(); +} + +SCurve SCurve::FromTransformationOf(SCurve *a, Vector t, + Quaternion q, double scale) +{ + bool needRotate = !EXACT(q.vx == 0.0 && q.vy == 0.0 && q.vz == 0.0 && q.w == 1.0); + bool needTranslate = !EXACT(t.x == 0.0 && t.y == 0.0 && t.z == 0.0); + bool needScale = !EXACT(scale == 1.0); + + SCurve ret = {}; + ret.h = a->h; + ret.isExact = a->isExact; + ret.exact = (a->exact).TransformedBy(t, q, scale); + ret.surfA = a->surfA; + ret.surfB = a->surfB; + + SCurvePt *p; + ret.pts.ReserveMore(a->pts.n); + for(p = a->pts.First(); p; p = a->pts.NextAfter(p)) { + SCurvePt pp = *p; + if(needScale) { + pp.p = (pp.p).ScaledBy(scale); + } + if(needRotate) { + pp.p = q.Rotate(pp.p); + } + if(needTranslate) { + pp.p = pp.p.Plus(t); + } + ret.pts.Add(&pp); + } + return ret; +} + +void SCurve::Clear() { + pts.Clear(); +} + +SSurface *SCurve::GetSurfaceA(SShell *a, SShell *b) const { + if(source == Source::A) { + return a->surface.FindById(surfA); + } else if(source == Source::B) { + return b->surface.FindById(surfA); + } else if(source == Source::INTERSECTION) { + return a->surface.FindById(surfA); + } else ssassert(false, "Unexpected curve source"); +} + +SSurface *SCurve::GetSurfaceB(SShell *a, SShell *b) const { + if(source == Source::A) { + return a->surface.FindById(surfB); + } else if(source == Source::B) { + return b->surface.FindById(surfB); + } else if(source == Source::INTERSECTION) { + return b->surface.FindById(surfB); + } else ssassert(false, "Unexpected curve source"); +} + +//----------------------------------------------------------------------------- +// When we split line segments wherever they intersect a surface, we introduce +// extra pwl points. This may create very short edges that could be removed +// without violating the chord tolerance. Those are ugly, and also break +// stuff in the Booleans. So remove them. +//----------------------------------------------------------------------------- +void SCurve::RemoveShortSegments(SSurface *srfA, SSurface *srfB) { + if(pts.n <= 2) return; + pts.ClearTags(); + + Vector prev = pts[0].p; + double tprev = 0; + double t = 0; + double tnext = 0; + + int i, a; + for(i = 1; i < pts.n - 1; i++) { + SCurvePt *sct = &(pts[i]), + *scn = &(pts[i+1]); + + if(sct->vertex) { + prev = sct->p; + continue; + } + + // if the curve is exact and points are >0.05 apart wrt t, point is there + // deliberately regardless of chord tolerance (ex: small circles) + tprev = t = tnext = 0; + if (isExact) { + exact.ClosestPointTo(prev, &tprev, /*mustconverge=*/ true); + exact.ClosestPointTo(sct->p, &t, /*mustconverge=*/ true); + exact.ClosestPointTo(scn->p, &tnext, /*mustconverge=*/ true); + } + if ( (t - tprev > 0.05) && (tnext - t > 0.05) ) { + prev = sct->p; + continue; + } + + bool mustKeep = false; + + // We must check against both surfaces; the piecewise linear edge + // may have a different chord tolerance in the two surfaces. (For + // example, a circle in the surface of a cylinder is just a straight + // line, so it always has perfect chord tol, but that circle in + // a plane is a circle so it doesn't). + for(a = 0; a < 2; a++) { + SSurface *srf = (a == 0) ? srfA : srfB; + Vector puv, nuv; + srf->ClosestPointTo(prev, &(puv.x), &(puv.y)); + srf->ClosestPointTo(scn->p, &(nuv.x), &(nuv.y)); + + if(srf->ChordToleranceForEdge(nuv, puv) > SS.ChordTolMm() ) { + mustKeep = true; + } + } + + if(mustKeep) { + prev = sct->p; + } else { + sct->tag = 1; + // and prev is unchanged, since there's no longer any point + // in between + } + } + + pts.RemoveTagged(); +} + +STrimBy STrimBy::EntireCurve(SShell *shell, hSCurve hsc, bool backwards) { + STrimBy stb = {}; + stb.curve = hsc; + SCurve *sc = shell->curve.FindById(hsc); + + if(backwards) { + stb.finish = sc->pts[0].p; + stb.start = sc->pts.Last()->p; + stb.backwards = true; + } else { + stb.start = sc->pts[0].p; + stb.finish = sc->pts.Last()->p; + stb.backwards = false; + } + + return stb; +} + +} // namespace SolveSpace diff --git a/src/srf/merge.cpp b/src/srf/merge.cpp new file mode 100644 index 0000000..03ef339 --- /dev/null +++ b/src/srf/merge.cpp @@ -0,0 +1,130 @@ +//----------------------------------------------------------------------------- +// Routines to merge multiple coincident surfaces (each with their own trim +// curves) into a single surface, with all of the trim curves. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +void SShell::MergeCoincidentSurfaces() { + surface.ClearTags(); + + int i, j; + SSurface *si, *sj; + + for(i = 0; i < surface.n; i++) { + si = &(surface[i]); + if(si->tag) continue; + // Let someone else clean up the empty surfaces; we can certainly merge + // them, but we don't know how to calculate a reasonable bounding box. + if(si->trim.IsEmpty()) + continue; + // And for now we handle only coincident planes, so no sense wasting + // time on other surfaces. + if(si->degm != 1 || si->degn != 1) continue; + + SEdgeList sel = {}; + si->MakeEdgesInto(this, &sel, SSurface::MakeAs::XYZ); + + bool mergedThisTime, merged = false; + do { + mergedThisTime = false; + + for(j = i + 1; j < surface.n; j++) { + sj = &(surface[j]); + if(sj->tag) continue; + if(!sj->CoincidentWith(si, /*sameNormal=*/true)) continue; + if(!sj->color.Equals(si->color)) continue; + // But we do merge surfaces with different face entities, since + // otherwise we'd hardly ever merge anything. + + // This surface is coincident. But let's not merge coincident + // surfaces if they contain disjoint contours; that just makes + // the bounding box tests less effective, and possibly things + // less robust. + SEdgeList tel = {}; + sj->MakeEdgesInto(this, &tel, SSurface::MakeAs::XYZ); + if(!sel.ContainsEdgeFrom(&tel)) { + tel.Clear(); + continue; + } + tel.Clear(); + + sj->tag = 1; + merged = true; + mergedThisTime = true; + sj->MakeEdgesInto(this, &sel, SSurface::MakeAs::XYZ); + sj->trim.Clear(); + + // All the references to this surface get replaced with the + // new srf + for(SCurve &sc : curve) { + if(sc.surfA == sj->h) sc.surfA = si->h; + if(sc.surfB == sj->h) sc.surfB = si->h; + } + } + + // If this iteration merged a contour onto ours, then we have to + // go through the surfaces again; that might have made a new + // surface touch us. + } while(mergedThisTime); + + if(merged) { + sel.CullExtraneousEdges(); + si->trim.Clear(); + si->TrimFromEdgeList(&sel, /*asUv=*/false); + + // And we must choose control points such that all the trims lie + // with u and v in [0, 1], so that the bbox tests work. + Vector u, v, n; + si->TangentsAt(0.5, 0.5, &u, &v); + u = u.WithMagnitude(1); + v = v.WithMagnitude(1); + n = si->NormalAt(0.5, 0.5).WithMagnitude(1); + v = (n.Cross(u)).WithMagnitude(1); + + double umax = VERY_NEGATIVE, umin = VERY_POSITIVE, + vmax = VERY_NEGATIVE, vmin = VERY_POSITIVE; + SEdge *se; + for(se = sel.l.First(); se; se = sel.l.NextAfter(se)) { + double ut = (se->a).Dot(u), vt = (se->a).Dot(v); + umax = max(umax, ut); + vmax = max(vmax, vt); + umin = min(umin, ut); + vmin = min(vmin, vt); + } + + // An interesting problem here; the real curve could extend + // slightly beyond the bounding box of the piecewise linear + // bits. Not a problem for us, but some apps won't import STEP + // in that case. So give a bit of extra room; in theory just + // a chord tolerance, but more can't hurt. + double muv = max((umax - umin), (vmax - vmin)); + double tol = muv/50 + 3*SS.ChordTolMm(); + umax += tol; + vmax += tol; + umin -= tol; + vmin -= tol; + + // We move in the +v direction as v goes from 0 to 1, and in the + // +u direction as u goes from 0 to 1. So our normal ends up + // pointed the same direction. + double nt = (si->ctrl[0][0]).Dot(n); + si->ctrl[0][0] = + Vector::From(umin, vmin, nt).ScaleOutOfCsys(u, v, n); + si->ctrl[0][1] = + Vector::From(umin, vmax, nt).ScaleOutOfCsys(u, v, n); + si->ctrl[1][1] = + Vector::From(umax, vmax, nt).ScaleOutOfCsys(u, v, n); + si->ctrl[1][0] = + Vector::From(umax, vmin, nt).ScaleOutOfCsys(u, v, n); + } + sel.Clear(); + } + + surface.RemoveTagged(); +} + +} // namespace SolveSpace diff --git a/src/srf/ratpoly.cpp b/src/srf/ratpoly.cpp new file mode 100644 index 0000000..20b8d19 --- /dev/null +++ b/src/srf/ratpoly.cpp @@ -0,0 +1,704 @@ +//----------------------------------------------------------------------------- +// Math on rational polynomial surfaces and curves, typically in Bezier +// form. Evaluate, root-find (by Newton's methods), evaluate derivatives, +// and so on. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +// Converge it to better than LENGTH_EPS; we want two points, each +// independently projected into uv and back, to end up equal with the +// LENGTH_EPS. Best case that requires LENGTH_EPS/2, but more is better +// and convergence should be fast by now. +#define RATPOLY_EPS (LENGTH_EPS/(1e2)) + +static inline double Bernstein(int k, int deg, double t) { +// indexed by [degree][k][exponent] + static const double bernstein_coeff[4][4][4] = { + { { 1.0,0.0,0.0,0.0 }, { 1.0,0.0,0.0,0.0 }, { 1.0,0.0,0.0,0.0 }, { 1.0,0.0,0.0,0.0 } }, + { { 1.0,-1.0,0.0,0.0 }, { 0.0,1.0,0.0,0.0 }, { 0.0,0.0,0.0,0.0 }, { 0.0,0.0,0.0,0.0 } }, + { { 1.0,-2.0,1.0,0.0 }, { 0.0,2.0,-2.0,0.0 },{ 0.0,0.0,1.0,0.0 }, { 0.0,0.0,0.0,0.0 } }, + { { 1.0,-3.0,3.0,-1.0 },{ 0.0,3.0,-6.0,3.0 },{ 0.0,0.0,3.0,-3.0}, { 0.0,0.0,0.0,1.0 } } }; + + const double *c = bernstein_coeff[deg][k]; + return (((c[3]*t+c[2])*t)+c[1])*t+c[0]; +} + +static inline double BernsteinDerivative(int k, int deg, double t) { + static const double bernstein_derivative_coeff[4][4][3] = { + { { 0.0,0.0,0.0 }, { 0.0,0.0,0.0 }, { 0.0,0.0,0.0 }, { 0.0,0.0,0.0 } }, + { { -1.0,0.0,0.0 }, { 1.0,0.0,0.0 }, { 0.0,0.0,0.0 }, { 0.0,0.0,0.0 } }, + { { -2.0,2.0,0.0 }, { 2.0,-4.0,0.0 },{ 0.0,2.0,0.0 }, { 0.0,0.0,0.0 } }, + { { -3.0,6.0,-3.0 },{ 3.0,-12.0,9.0 },{ 0.0,6.0,-9.0}, { 0.0,0.0,3.0 } } }; + + const double *c = bernstein_derivative_coeff[deg][k]; + return ((c[2]*t)+c[1])*t+c[0]; +} + +Vector SBezier::PointAt(double t) const { + Vector pt = Vector::From(0, 0, 0); + double d = 0; + + int i; + for(i = 0; i <= deg; i++) { + double B = Bernstein(i, deg, t); + pt = pt.Plus(ctrl[i].ScaledBy(B*weight[i])); + d += weight[i]*B; + } + pt = pt.ScaledBy(1.0/d); + return pt; +} + +Vector SBezier::TangentAt(double t) const { + Vector pt = Vector::From(0, 0, 0), pt_p = Vector::From(0, 0, 0); + double d = 0, d_p = 0; + + int i; + for(i = 0; i <= deg; i++) { + double B = Bernstein(i, deg, t), + Bp = BernsteinDerivative(i, deg, t); + + pt = pt.Plus(ctrl[i].ScaledBy(B*weight[i])); + d += weight[i]*B; + + pt_p = pt_p.Plus(ctrl[i].ScaledBy(Bp*weight[i])); + d_p += weight[i]*Bp; + } + + // quotient rule; f(t) = n(t)/d(t), so f' = (n'*d - n*d')/(d^2) + Vector ret; + ret = (pt_p.ScaledBy(d)).Minus(pt.ScaledBy(d_p)); + ret = ret.ScaledBy(1.0/(d*d)); + return ret; +} + +void SBezier::ClosestPointTo(Vector p, double *t, bool mustConverge) const { + int i; + double minDist = VERY_POSITIVE; + *t = 0; + double res = (deg <= 2) ? 7.0 : 20.0; + for(i = 0; i < (int)res; i++) { + double tryt = (i/res); + + Vector tryp = PointAt(tryt); + double d = (tryp.Minus(p)).Magnitude(); + if(d < minDist) { + *t = tryt; + minDist = d; + } + } + + Vector p0; + for(i = 0; i < (mustConverge ? 15 : 5); i++) { + p0 = PointAt(*t); + if(p0.Equals(p, RATPOLY_EPS)) { + return; + } + + Vector dp = TangentAt(*t); + Vector pc = p.ClosestPointOnLine(p0, dp); + *t += (pc.Minus(p0)).DivProjected(dp); + } + if(mustConverge) { + dbp("didn't converge (closest point on bezier curve)"); + } +} + +bool SBezier::PointOnThisAndCurve(const SBezier *sbb, Vector *p) const { + double ta, tb; + this->ClosestPointTo(*p, &ta, /*mustConverge=*/false); + sbb ->ClosestPointTo(*p, &tb, /*mustConverge=*/false); + + int i; + for(i = 0; i < 20; i++) { + Vector pa = this->PointAt(ta), + pb = sbb ->PointAt(tb), + da = this->TangentAt(ta), + db = sbb ->TangentAt(tb); + + if(pa.Equals(pb, RATPOLY_EPS)) { + *p = pa; + return true; + } + + double tta, ttb; + Vector::ClosestPointBetweenLines(pa, da, pb, db, &tta, &ttb); + ta += tta; + tb += ttb; + } + return false; +} + +void SBezier::SplitAt(double t, SBezier *bef, SBezier *aft) const { + Vector4 ct[4]; + int i; + for(i = 0; i <= deg; i++) { + ct[i] = Vector4::From(weight[i], ctrl[i]); + } + + switch(deg) { + case 1: { + Vector4 cts = Vector4::Blend(ct[0], ct[1], t); + *bef = SBezier::From(ct[0], cts); + *aft = SBezier::From(cts, ct[1]); + break; + } + case 2: { + Vector4 ct01 = Vector4::Blend(ct[0], ct[1], t), + ct12 = Vector4::Blend(ct[1], ct[2], t), + cts = Vector4::Blend(ct01, ct12, t); + + *bef = SBezier::From(ct[0], ct01, cts); + *aft = SBezier::From(cts, ct12, ct[2]); + break; + } + case 3: { + Vector4 ct01 = Vector4::Blend(ct[0], ct[1], t), + ct12 = Vector4::Blend(ct[1], ct[2], t), + ct23 = Vector4::Blend(ct[2], ct[3], t), + ct01_12 = Vector4::Blend(ct01, ct12, t), + ct12_23 = Vector4::Blend(ct12, ct23, t), + cts = Vector4::Blend(ct01_12, ct12_23, t); + + *bef = SBezier::From(ct[0], ct01, ct01_12, cts); + *aft = SBezier::From(cts, ct12_23, ct23, ct[3]); + break; + } + default: ssassert(false, "Unexpected degree of spline"); + } +} + +void SBezier::MakePwlInto(SEdgeList *sel, double chordTol, double max_dt) const { + List lv = {}; + MakePwlInto(&lv, chordTol, max_dt); + int i; + for(i = 1; i < lv.n; i++) { + sel->AddEdge(lv[i-1], lv[i]); + } + lv.Clear(); +} +void SBezier::MakePwlInto(List *l, double chordTol, double max_dt) const { + List lv = {}; + MakePwlInto(&lv, chordTol, max_dt); + int i; + for(i = 0; i < lv.n; i++) { + SCurvePt scpt; + scpt.tag = 0; + scpt.p = lv[i]; + scpt.vertex = (i == 0) || (i == (lv.n - 1)); + l->Add(&scpt); + } + lv.Clear(); +} +void SBezier::MakePwlInto(SContour *sc, double chordTol, double max_dt) const { + List lv = {}; + MakePwlInto(&lv, chordTol, max_dt); + int i; + for(i = 0; i < lv.n; i++) { + sc->AddPoint(lv[i]); + } + lv.Clear(); +} +//-------------------------------------------------------------------------------------- +// all variants of MakePwlInto come here. Split a rational Bezier into Piecewise Linear +// segments that don't deviate from the actual curve by more than the chordTol distance. +// max_dt allows to force curves to be split into spans of no more than a certain +// length based on t-parameter. RemoveShortSegments() may delete points when dt <= 0.1 +//-------------------------------------------------------------------------------------- +void SBezier::MakePwlInto(List *l, double chordTol, double max_dt) const { + if(EXACT(chordTol == 0)) { + // Use the default chord tolerance. + chordTol = SS.ChordTolMm(); + } + // Never do fewer than three intermediate points for curves; people seem to get + // unhappy when their circles turn into squares, but maybe less + // unhappy with octagons. Now 16-gons. + if (EXACT(max_dt == 0.0)) { + max_dt = (deg == 1) ? 1.0 : 0.25; + } + l->Add(&(ctrl[0])); + // don't split first degee (lines) unless asked to by the caller via max_dt + if((deg == 1) && (max_dt >= 1.0)) { + l->Add(&(ctrl[1])); + } else { + MakePwlInitialWorker(l, 0.0, 0.5, chordTol, max_dt); + MakePwlInitialWorker(l, 0.5, 1.0, chordTol, max_dt); + } +} +void SBezier::MakePwlWorker(List *l, double ta, double tb, double chordTol, double max_dt) const +{ + Vector pa = PointAt(ta); + Vector pb = PointAt(tb); + + Vector pm = PointAt((ta + tb) / 2.0); + double d = pm.DistanceToLine(pa, pb.Minus(pa)); + + double step = 1.0/SS.GetMaxSegments(); + if(((tb - ta) < step || d < chordTol) && ((tb-ta) <= max_dt) ) { + // A previous call has already added the beginning of our interval. + l->Add(&pb); + } else { + double tm = (ta + tb) / 2; + MakePwlWorker(l, ta, tm, chordTol, max_dt); + MakePwlWorker(l, tm, tb, chordTol, max_dt); + } +} +void SBezier::MakePwlInitialWorker(List *l, double ta, double tb, double chordTol, double max_dt) const +{ + Vector pa = PointAt(ta); + Vector pb = PointAt(tb); + + double tm1 = ta + (tb - ta) * 0.25; + double tm2 = ta + (tb - ta) * 0.5; + double tm3 = ta + (tb - ta) * 0.75; + + Vector pm1 = PointAt(tm1); + Vector pm2 = PointAt(tm2); + Vector pm3 = PointAt(tm3); + Vector dir = pb.Minus(pa); + + double d = max({ + pm1.DistanceToLine(pa, dir), + pm2.DistanceToLine(pa, dir), + pm3.DistanceToLine(pa, dir) + }); + + double step = 1.0/SS.GetMaxSegments(); + if( ((tb - ta) < step || d < chordTol) && ((tb-ta) <= max_dt) ) { + // A previous call has already added the beginning of our interval. + l->Add(&pb); + } else { + double tm = (ta + tb) / 2; + MakePwlWorker(l, ta, tm, chordTol, max_dt); + MakePwlWorker(l, tm, tb, chordTol, max_dt); + } +} + +void SBezier::MakeNonrationalCubicInto(SBezierList *bl, double tolerance, int depth) const { + Vector t0 = TangentAt(0), t1 = TangentAt(1); + // The curve is correct, and the first derivatives are correct, at the + // endpoints. + SBezier bnr = SBezier::From( + Start(), + Start().Plus(t0.ScaledBy(1.0/3)), + Finish().Minus(t1.ScaledBy(1.0/3)), + Finish()); + + bool closeEnough = true; + int i; + for(i = 1; i <= 3; i++) { + double t = i/4.0; + Vector p0 = PointAt(t), + pn = bnr.PointAt(t); + double d = (p0.Minus(pn)).Magnitude(); + if(d > tolerance) { + closeEnough = false; + } + } + + if(closeEnough || depth > 3) { + bl->l.Add(this); + } else { + SBezier bef, aft; + SplitAt(0.5, &bef, &aft); + bef.MakeNonrationalCubicInto(bl, tolerance, depth+1); + aft.MakeNonrationalCubicInto(bl, tolerance, depth+1); + } +} + +Vector SSurface::PointAt(Point2d puv) const { + return PointAt(puv.x, puv.y); +} +Vector SSurface::PointAt(double u, double v) const { + Vector num = Vector::From(0, 0, 0); + double den = 0; + + int i, j; + for(i = 0; i <= degm; i++) { + for(j = 0; j <= degn; j++) { + double Bi = Bernstein(i, degm, u), + Bj = Bernstein(j, degn, v); + + num = num.Plus(ctrl[i][j].ScaledBy(Bi*Bj*weight[i][j])); + den += weight[i][j]*Bi*Bj; + } + } + num = num.ScaledBy(1.0/den); + return num; +} + +void SSurface::TangentsAt(double u, double v, Vector *tu, Vector *tv, bool retry) const { + Vector num = Vector::From(0, 0, 0), + num_u = Vector::From(0, 0, 0), + num_v = Vector::From(0, 0, 0); + double den = 0, + den_u = 0, + den_v = 0; + + int i, j; + for(i = 0; i <= degm; i++) { + for(j = 0; j <= degn; j++) { + double Bi = Bernstein(i, degm, u), + Bj = Bernstein(j, degn, v), + Bip = BernsteinDerivative(i, degm, u), + Bjp = BernsteinDerivative(j, degn, v); + + num = num.Plus(ctrl[i][j].ScaledBy(Bi*Bj*weight[i][j])); + den += weight[i][j]*Bi*Bj; + + num_u = num_u.Plus(ctrl[i][j].ScaledBy(Bip*Bj*weight[i][j])); + den_u += weight[i][j]*Bip*Bj; + + num_v = num_v.Plus(ctrl[i][j].ScaledBy(Bi*Bjp*weight[i][j])); + den_v += weight[i][j]*Bi*Bjp; + } + } + // quotient rule; f(t) = n(t)/d(t), so f' = (n'*d - n*d')/(d^2) + *tu = ((num_u.ScaledBy(den)).Minus(num.ScaledBy(den_u))); + *tu = tu->ScaledBy(1.0/(den*den)); + + *tv = ((num_v.ScaledBy(den)).Minus(num.ScaledBy(den_v))); + *tv = tv->ScaledBy(1.0/(den*den)); + + // Tangent is zero at sungularities like the north pole. Move away a bit and retry. + if(tv->Equals(Vector::From(0,0,0)) && retry) + TangentsAt(u+(0.5-u)*0.00001, v, tu, tv, false); + if(tu->Equals(Vector::From(0,0,0)) && retry) + TangentsAt(u, v+(0.5-v)*0.00001, tu, tv, false); +} + +Vector SSurface::NormalAt(Point2d puv) const { + return NormalAt(puv.x, puv.y); +} + +Vector SSurface::NormalAt(double u, double v) const { + Vector tu, tv; + TangentsAt(u, v, &tu, &tv); + return tu.Cross(tv); +} + +void SSurface::ClosestPointTo(Vector p, Point2d *puv, bool mustConverge) { + ClosestPointTo(p, &(puv->x), &(puv->y), mustConverge); +} + +void SSurface::ClosestPointTo(Vector p, double *u, double *v, bool mustConverge) { + // A few special cases first; when control points are coincident the + // derivative goes to zero at the control points, and would result in + // nonconvergence. We avoid that here, and also guarantee a consistent + // (u, v) (of the infinitely many possible in one parameter). + if(p.Equals(ctrl[0] [0] )) { *u = 0; *v = 0; return; } + if(p.Equals(ctrl[degm][0] )) { *u = 1; *v = 0; return; } + if(p.Equals(ctrl[degm][degn])) { *u = 1; *v = 1; return; } + if(p.Equals(ctrl[0] [degn])) { *u = 0; *v = 1; return; } + + // And planes are trivial, so don't waste time iterating over those. + if(degm == 1 && degn == 1) { + Vector orig = ctrl[0][0], + bu = (ctrl[1][0]).Minus(orig), + bv = (ctrl[0][1]).Minus(orig); + if((ctrl[1][1]).Equals(orig.Plus(bu).Plus(bv))) { + + Vector n = bu.Cross(bv); + Vector ty = n.Cross(bu).ScaledBy(1.0/bu.MagSquared()); + Vector tx = bv.Cross(n).ScaledBy(1.0/bv.MagSquared()); + + Vector dp = p.Minus(orig); + *u = dp.Dot(bu) / tx.MagSquared(); + *v = dp.Dot(bv) / ty.MagSquared(); + return; + } + } + + // Try whatever the previous guess was. This is likely to do something + // good if we're working our way along a curve or something else where + // we project successive points that are close to each other; something + // like a 20% speedup empirically. + if(mustConverge) { + double ut = cached.x, vt = cached.y; + if(ClosestPointNewton(p, &ut, &vt, mustConverge)) { + cached.x = *u = ut; + cached.y = *v = vt; + return; + } + } + + // Search for a reasonable initial guess + int i, j; + double minDist = VERY_POSITIVE; + int res = (max(degm, degn) == 2) ? 7 : 20; + for(i = 0; i < res; i++) { + for(j = 0; j < res; j++) { + double tryu = (i + 0.5)/res, tryv = (j + 0.5)/res; + + Vector tryp = PointAt(tryu, tryv); + double d = (tryp.Minus(p)).Magnitude(); + if(d < minDist) { + *u = tryu; + *v = tryv; + minDist = d; + } + } + } + + if(ClosestPointNewton(p, u, v, mustConverge)) { + cached.x = *u; + cached.y = *v; + return; + } + + // If we failed to converge, then at least don't return NaN. + if(mustConverge) { +// This is expected not to converge when the target point is not on the surface but nearby. +// let's not pollute the output window for normal use. +// Vector p0 = PointAt(*u, *v); +// dbp("didn't converge"); +// dbp("have %.3f %.3f %.3f", CO(p0)); +// dbp("want %.3f %.3f %.3f", CO(p)); +// dbp("distance = %g", (p.Minus(p0)).Magnitude()); + } + if(IsReasonable(*u) || IsReasonable(*v)) { + *u = *v = 0; + } +} + +bool SSurface::ClosestPointNewton(Vector p, double *u, double *v, bool mustConverge) const +{ + // Initial guess is in u, v; refine by Newton iteration. + Vector p0 = Vector::From(0, 0, 0); + for(int i = 0; i < (mustConverge ? 25 : 5); i++) { + p0 = PointAt(*u, *v); + if(mustConverge) { + if(p0.Equals(p, RATPOLY_EPS)) { + return true; + } + } + + Vector tu, tv, tx, ty; + TangentsAt(*u, *v, &tu, &tv); + Vector n = tu.Cross(tv); + // since tu and tv may not be orthogonal, use y in place of v. + // |y| = |v|sin(theta) where theta is the angle between tu and tv. + ty = n.Cross(tu).ScaledBy(1.0/tu.MagSquared()); + tx = tv.Cross(n).ScaledBy(1.0/tv.MagSquared()); + + // Project the point into a plane through p0, with basis tu, tv; a + // second-order thing would converge faster but needs second + // derivatives. + Vector dp = p.Minus(p0); + double du = dp.Dot(tx), + dv = dp.Dot(ty); + *u += du / (tx.MagSquared()); + *v += dv / (ty.MagSquared()); + + if (*u < 0.0) *u = 0.0; + else if (*u > 1.0) *u = 1.0; + if (*v < 0.0) *v = 0.0; + else if (*v > 1.0) *v = 1.0; + + } + + return false; +} + +bool SSurface::PointIntersectingLine(Vector p0, Vector p1, double *u, double *v) const +{ + int i; + for(i = 0; i < 20; i++) { + Vector pi, p, tu, tv; + p = PointAt(*u, *v); + TangentsAt(*u, *v, &tu, &tv); + + Vector n = (tu.Cross(tv)).WithMagnitude(1); + double d = p.Dot(n); + + bool parallel; + pi = Vector::AtIntersectionOfPlaneAndLine(n, d, p0, p1, ¶llel); + if(parallel) { + dbp("parallel (surface intersecting line)"); + break; + } + + // Check for convergence + if(pi.Equals(p, RATPOLY_EPS)) return true; + + n = tu.Cross(tv); + Vector ty = n.Cross(tu).ScaledBy(1.0/tu.MagSquared()); + Vector tx = tv.Cross(n).ScaledBy(1.0/tv.MagSquared()); + + // Adjust our guess and iterate + Vector dp = pi.Minus(p); + double du = dp.Dot(tx), dv = dp.Dot(ty); + *u += du / tx.MagSquared(); + *v += dv / ty.MagSquared(); + } + dbp("didn't converge (surface intersecting line)"); + return false; +} + +Vector SSurface::ClosestPointOnThisAndSurface(SSurface *srf2, Vector p) { + // This is untested. + int i, j; + Point2d puv[2]; + SSurface *srf[2] = { this, srf2 }; + + for(j = 0; j < 2; j++) { + (srf[j])->ClosestPointTo(p, &(puv[j]), /*mustConverge=*/false); + } + + for(i = 0; i < 10; i++) { + Vector tu[2], tv[2], cp[2], n[2]; + double d[2]; + + for(j = 0; j < 2; j++) { + (srf[j])->TangentsAt(puv[j].x, puv[j].y, &(tu[j]), &(tv[j])); + + cp[j] = (srf[j])->PointAt(puv[j]); + + n[j] = ((tu[j]).Cross(tv[j])).WithMagnitude(1); + d[j] = (n[j]).Dot(cp[j]); + } + + if((cp[0]).Equals(cp[1], RATPOLY_EPS)) break; + + Vector p0 = Vector::AtIntersectionOfPlanes(n[0], d[0], n[1], d[1]), + dp = (n[0]).Cross(n[1]); + + Vector pc = p.ClosestPointOnLine(p0, dp); + + // Adjust our guess and iterate + for(j = 0; j < 2; j++) { + Vector n = tu[j].Cross(tv[j]); + Vector ty = n.Cross(tu[j]).ScaledBy(1.0/tu[j].MagSquared()); + Vector tx = tv[j].Cross(n).ScaledBy(1.0/tv[j].MagSquared()); + + Vector dc = pc.Minus(cp[j]); + double du = dc.Dot(tx), dv = dc.Dot(ty); + puv[j].x += du / tx.MagSquared(); + puv[j].y += dv / ty.MagSquared(); + } + } + if(i >= 10) { + dbp("this and srf, didn't converge, d=%g", + (puv[0].Minus(puv[1])).Magnitude()); + } + + // If this converged, then the two points are actually equal. + return ((srf[0])->PointAt(puv[0])).Plus( + ((srf[1])->PointAt(puv[1]))).ScaledBy(0.5); +} + +void SSurface::PointOnSurfaces(SSurface *s1, SSurface *s2, double *up, double *vp) +{ + double u[3] = { *up, 0, 0 }, v[3] = { *vp, 0, 0 }; + SSurface *srf[3] = { this, s1, s2 }; + + // Get initial guesses for (u, v) in the other surfaces + Vector p = PointAt(*u, *v); + (srf[1])->ClosestPointTo(p, &(u[1]), &(v[1]), /*mustConverge=*/false); + (srf[2])->ClosestPointTo(p, &(u[2]), &(v[2]), /*mustConverge=*/false); + + int i, j; + for(i = 0; i < 20; i++) { + // Approximate each surface by a plane + Vector p[3], tu[3], tv[3], n[3]; + double d[3]; + for(j = 0; j < 3; j++) { + p[j] = (srf[j])->PointAt(u[j], v[j]); + (srf[j])->TangentsAt(u[j], v[j], &(tu[j]), &(tv[j])); + n[j] = ((tu[j]).Cross(tv[j])).WithMagnitude(1); + d[j] = (n[j]).Dot(p[j]); + } + + // If a = b and b = c, then does a = c? No, it doesn't. + if((p[0]).Equals(p[1], RATPOLY_EPS) && + (p[1]).Equals(p[2], RATPOLY_EPS) && + (p[2]).Equals(p[0], RATPOLY_EPS)) + { + *up = u[0]; + *vp = v[0]; + return; + } + + bool parallel; + Vector pi = Vector::AtIntersectionOfPlanes(n[0], d[0], + n[1], d[1], + n[2], d[2], ¶llel); + + if(parallel) { // lets try something else for parallel planes + pi = p[0].Plus(p[1]).Plus(p[2]).ScaledBy(1.0/3.0); + } + + for(j = 0; j < 3; j++) { + Vector n = tu[j].Cross(tv[j]); + Vector ty = n.Cross(tu[j]).ScaledBy(1.0/tu[j].MagSquared()); + Vector tx = tv[j].Cross(n).ScaledBy(1.0/tv[j].MagSquared()); + + Vector dp = pi.Minus(p[j]); + double du = dp.Dot(tx), dv = dp.Dot(ty); + + u[j] += du / tx.MagSquared(); + v[j] += dv / ty.MagSquared(); + } + } + dbp("didn't converge (three surfaces intersecting)"); +} + +void SSurface::PointOnCurve(const SBezier *curve, double *up, double *vp) +{ + Vector tu,tv,n; + double u = *up, v = *vp; + Vector ps = PointAt(u, v); + // Get initial guesses for t on the curve + double tCurve = 0.5; + curve->ClosestPointTo(ps, &tCurve, /*mustConverge=*/false); + if(tCurve < 0.0) tCurve = 0.0; + if(tCurve > 1.0) tCurve = 1.0; + + for(int i = 0; i < 30; i++) { + // Approximate the surface by a plane + Vector ps = PointAt(u, v); + TangentsAt(u, v, &tu, &tv); + n = tu.Cross(tv).WithMagnitude(1); + + // point on curve and tangent line direction + Vector pc = curve->PointAt(tCurve); + Vector tc = curve->TangentAt(tCurve); + + if(ps.Equals(pc, RATPOLY_EPS)) { + *up = u; + *vp = v; + return; + } + + //pi is where the curve tangent line intersects the surface tangent plane + Vector pi; + double d = tc.Dot(n); + if (fabs(d) < 1e-10) { // parallel line and plane, guess the average rather than fail + pi = pc.Plus(ps).ScaledBy(0.5); + } else { + pi = pc.Minus(tc.ScaledBy(pc.Minus(ps).Dot(n)/d)); + } + + // project the point onto the tangent plane and line + { + Vector n = tu.Cross(tv); + Vector ty = n.Cross(tu).ScaledBy(1.0/tu.MagSquared()); + Vector tx = tv.Cross(n).ScaledBy(1.0/tv.MagSquared()); + + Vector dp = pi.Minus(ps); + double du = dp.Dot(tx), dv = dp.Dot(ty); + + u += du / tx.MagSquared(); + v += dv / ty.MagSquared(); + } + tCurve += pi.Minus(pc).Dot(tc) / tc.MagSquared(); + if(tCurve < 0.0) tCurve = 0.0; + if(tCurve > 1.0) tCurve = 1.0; + } + dbp("didn't converge (surface and curve intersecting)"); +} + +} // namespace SolveSpace diff --git a/src/srf/raycast.cpp b/src/srf/raycast.cpp new file mode 100644 index 0000000..4c2f435 --- /dev/null +++ b/src/srf/raycast.cpp @@ -0,0 +1,616 @@ +//----------------------------------------------------------------------------- +// Routines for ray-casting: intersecting a line segment or an infinite line +// with a surface or shell. Ray-casting against a shell is used for point-in- +// shell testing, and the intersection of edge line segments against surfaces +// is used to get rough surface-curve intersections, which are later refined +// numerically. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +// Dot product tolerance for perpendicular; this is on the direction cosine, +// so it's about 0.001 degrees. +const double SShell::DOTP_TOL = 1e-5; + +extern int FLAG; + + +double SSurface::DepartureFromCoplanar() const { + int i, j; + int ia, ja, ib = 0, jb = 0, ic = 0, jc = 0; + double best; + + // Grab three points to define a plane; first choose (0, 0) arbitrarily. + ia = ja = 0; + // Then the point farthest from pt a. + best = VERY_NEGATIVE; + for(i = 0; i <= degm; i++) { + for(j = 0; j <= degn; j++) { + if(i == ia && j == ja) continue; + + double dist = (ctrl[i][j]).Minus(ctrl[ia][ja]).Magnitude(); + if(dist > best) { + best = dist; + ib = i; + jb = j; + } + } + } + // Then biggest magnitude of ab cross ac. + best = VERY_NEGATIVE; + for(i = 0; i <= degm; i++) { + for(j = 0; j <= degn; j++) { + if(i == ia && j == ja) continue; + if(i == ib && j == jb) continue; + + double mag = + ((ctrl[ia][ja].Minus(ctrl[ib][jb]))).Cross( + (ctrl[ia][ja].Minus(ctrl[i ][j ]))).Magnitude(); + if(mag > best) { + best = mag; + ic = i; + jc = j; + } + } + } + + Vector n = ((ctrl[ia][ja].Minus(ctrl[ib][jb]))).Cross( + (ctrl[ia][ja].Minus(ctrl[ic][jc]))); + n = n.WithMagnitude(1); + double d = (ctrl[ia][ja]).Dot(n); + + // Finally, calculate the deviation from each point to the plane. + double farthest = VERY_NEGATIVE; + for(i = 0; i <= degm; i++) { + for(j = 0; j <= degn; j++) { + double dist = fabs(n.Dot(ctrl[i][j]) - d); + if(dist > farthest) { + farthest = dist; + } + } + } + return farthest; +} + +void SSurface::WeightControlPoints() { + int i, j; + for(i = 0; i <= degm; i++) { + for(j = 0; j <= degn; j++) { + ctrl[i][j] = (ctrl[i][j]).ScaledBy(weight[i][j]); + } + } +} +void SSurface::UnWeightControlPoints() { + int i, j; + for(i = 0; i <= degm; i++) { + for(j = 0; j <= degn; j++) { + ctrl[i][j] = (ctrl[i][j]).ScaledBy(1.0/weight[i][j]); + } + } +} +void SSurface::CopyRowOrCol(bool row, int this_ij, SSurface *src, int src_ij) { + if(row) { + int j; + for(j = 0; j <= degn; j++) { + ctrl [this_ij][j] = src->ctrl [src_ij][j]; + weight[this_ij][j] = src->weight[src_ij][j]; + } + } else { + int i; + for(i = 0; i <= degm; i++) { + ctrl [i][this_ij] = src->ctrl [i][src_ij]; + weight[i][this_ij] = src->weight[i][src_ij]; + } + } +} +void SSurface::BlendRowOrCol(bool row, int this_ij, SSurface *a, int a_ij, + SSurface *b, int b_ij) +{ + if(row) { + int j; + for(j = 0; j <= degn; j++) { + Vector c = (a->ctrl [a_ij][j]).Plus(b->ctrl [b_ij][j]); + double w = (a->weight[a_ij][j] + b->weight[b_ij][j]); + ctrl [this_ij][j] = c.ScaledBy(0.5); + weight[this_ij][j] = w / 2; + } + } else { + int i; + for(i = 0; i <= degm; i++) { + Vector c = (a->ctrl [i][a_ij]).Plus(b->ctrl [i][b_ij]); + double w = (a->weight[i][a_ij] + b->weight[i][b_ij]); + ctrl [i][this_ij] = c.ScaledBy(0.5); + weight[i][this_ij] = w / 2; + } + } +} +void SSurface::SplitInHalf(bool byU, SSurface *sa, SSurface *sb) { + sa->degm = sb->degm = degm; + sa->degn = sb->degn = degn; + + // by de Casteljau's algorithm in a projective space; so we must work + // on points (w*x, w*y, w*z, w) so create a temporary copy + SSurface st; + st = *this; + st.WeightControlPoints(); + + switch(byU ? degm : degn) { + case 1: + sa->CopyRowOrCol (byU, 0, &st, 0); + sb->CopyRowOrCol (byU, 1, &st, 1); + + sa->BlendRowOrCol(byU, 1, &st, 0, &st, 1); + sb->BlendRowOrCol(byU, 0, &st, 0, &st, 1); + break; + + case 2: + sa->CopyRowOrCol (byU, 0, &st, 0); + sb->CopyRowOrCol (byU, 2, &st, 2); + + sa->BlendRowOrCol(byU, 1, &st, 0, &st, 1); + sb->BlendRowOrCol(byU, 1, &st, 1, &st, 2); + + sa->BlendRowOrCol(byU, 2, sa, 1, sb, 1); + sb->BlendRowOrCol(byU, 0, sa, 1, sb, 1); + break; + + case 3: { + sa->CopyRowOrCol (byU, 0, &st, 0); + sb->CopyRowOrCol (byU, 3, &st, 3); + + sa->BlendRowOrCol(byU, 1, &st, 0, &st, 1); + sb->BlendRowOrCol(byU, 2, &st, 2, &st, 3); + st. BlendRowOrCol(byU, 0, &st, 1, &st, 2); // use row/col 0 as scratch + + sa->BlendRowOrCol(byU, 2, sa, 1, &st, 0); + sb->BlendRowOrCol(byU, 1, sb, 2, &st, 0); + + sa->BlendRowOrCol(byU, 3, sa, 2, sb, 1); + sb->BlendRowOrCol(byU, 0, sa, 2, sb, 1); + break; + } + + default: ssassert(false, "Unexpected degree of spline"); + } + + sa->UnWeightControlPoints(); + sb->UnWeightControlPoints(); +} + +//----------------------------------------------------------------------------- +// Find all points where the indicated finite (if segment) or infinite (if not +// segment) line intersects our surface. Report them in uv space in the list. +// We first do a bounding box check; if the line doesn't intersect, then we're +// done. If it does, then we check how small our surface is. If it's big, +// then we subdivide into quarters and recurse. If it's small, then we refine +// by Newton's method and record the point. +//----------------------------------------------------------------------------- +void SSurface::AllPointsIntersectingUntrimmed(Vector a, Vector b, + int *cnt, int *level, + List *l, bool asSegment, + SSurface *sorig) +{ + // Test if the line intersects our axis-aligned bounding box; if no, then + // no possibility of an intersection + if(LineEntirelyOutsideBbox(a, b, asSegment)) return; + + if(*cnt > 2000) { + dbp("!!! too many subdivisions (level=%d)!", *level); + dbp("degm = %d degn = %d", degm, degn); + return; + } + (*cnt)++; + + // If we might intersect, and the surface is small, then switch to Newton + // iterations. + if(DepartureFromCoplanar() < 0.2*SS.ChordTolMm()) { + Vector p = (ctrl[0 ][0 ]).Plus( + ctrl[0 ][degn]).Plus( + ctrl[degm][0 ]).Plus( + ctrl[degm][degn]).ScaledBy(0.25); + Inter inter; + sorig->ClosestPointTo(p, &(inter.p.x), &(inter.p.y), /*mustConverge=*/false); + if(sorig->PointIntersectingLine(a, b, &(inter.p.x), &(inter.p.y))) { + Vector p = sorig->PointAt(inter.p.x, inter.p.y); + // Debug check, verify that the point lies in both surfaces + // (which it ought to, since the surfaces should be coincident) + double u, v; + ClosestPointTo(p, &u, &v); + l->Add(&inter); + } else { + // Might not converge if line is almost tangent to surface... + } + return; + } + + // But the surface is big, so split it, alternating by u and v + SSurface surf0, surf1; + SplitInHalf((*level & 1) == 0, &surf0, &surf1); + + int nextLevel = (*level) + 1; + (*level) = nextLevel; + surf0.AllPointsIntersectingUntrimmed(a, b, cnt, level, l, asSegment, sorig); + (*level) = nextLevel; + surf1.AllPointsIntersectingUntrimmed(a, b, cnt, level, l, asSegment, sorig); +} + +//----------------------------------------------------------------------------- +// Find all points where a line through a and b intersects our surface, and +// add them to the list. If seg is true then report only intersections that +// lie within the finite line segment (not including the endpoints); otherwise +// we work along the infinite line. And we report either just intersections +// inside the trim curve, or any intersection with u, v in [0, 1]. And we +// either disregard or report tangent points. +//----------------------------------------------------------------------------- +void SSurface::AllPointsIntersecting(Vector a, Vector b, + List *l, + bool asSegment, bool trimmed, bool inclTangent) +{ + if(LineEntirelyOutsideBbox(a, b, asSegment)) return; + + Vector ba = b.Minus(a); + double bam = ba.Magnitude(); + + List inters = {}; + + // All the intersections between the line and the surface; either special + // cases that we can quickly solve in closed form, or general numerical. + Vector center, axis, start, finish; + double radius; + if(degm == 1 && degn == 1) { + // Against a plane, easy. + Vector n = NormalAt(0, 0).WithMagnitude(1); + double d = n.Dot(PointAt(0, 0)); + // Trim to line segment now if requested, don't generate points that + // would just get discarded later. + if(!asSegment || + (n.Dot(a) > d + LENGTH_EPS && n.Dot(b) < d - LENGTH_EPS) || + (n.Dot(b) > d + LENGTH_EPS && n.Dot(a) < d - LENGTH_EPS)) + { + Vector p = Vector::AtIntersectionOfPlaneAndLine(n, d, a, b, NULL); + Inter inter; + ClosestPointTo(p, &(inter.p.x), &(inter.p.y)); + inters.Add(&inter); + } + } else if(IsCylinder(&axis, ¢er, &radius, &start, &finish)) { + // This one can be solved in closed form too. + Vector ab = b.Minus(a); + if(axis.Cross(ab).Magnitude() < LENGTH_EPS) { + // edge is parallel to axis of cylinder, no intersection points + return; + } + // A coordinate system centered at the center of the circle, with + // the edge under test horizontal + Vector u, v, n = axis.WithMagnitude(1); + u = (ab.Minus(n.ScaledBy(ab.Dot(n)))).WithMagnitude(1); + v = n.Cross(u); + Point2d ap = (a.Minus(center)).DotInToCsys(u, v, n).ProjectXy(), + bp = (b.Minus(center)).DotInToCsys(u, v, n).ProjectXy(), + sp = (start. Minus(center)).DotInToCsys(u, v, n).ProjectXy(), + fp = (finish.Minus(center)).DotInToCsys(u, v, n).ProjectXy(); + + double thetas = atan2(sp.y, sp.x), thetaf = atan2(fp.y, fp.x); + + Point2d ip[2]; + int ip_n = 0; + if(fabs(fabs(ap.y) - radius) < LENGTH_EPS) { + // tangent + if(inclTangent) { + ip[0] = Point2d::From(0, ap.y); + ip_n = 1; + } + } else if(fabs(ap.y) < radius) { + // two intersections + double xint = sqrt(radius*radius - ap.y*ap.y); + ip[0] = Point2d::From(-xint, ap.y); + ip[1] = Point2d::From( xint, ap.y); + ip_n = 2; + } + int i; + for(i = 0; i < ip_n; i++) { + double t = (ip[i].Minus(ap)).DivProjected(bp.Minus(ap)); + // This is a point on the circle; but is it on the arc? + Point2d pp = ap.Plus((bp.Minus(ap)).ScaledBy(t)); + double theta = atan2(pp.y, pp.x); + double dp = WRAP_SYMMETRIC(theta - thetas, 2*PI), + df = WRAP_SYMMETRIC(thetaf - thetas, 2*PI); + double tol = LENGTH_EPS/radius; + + if((df > 0 && ((dp < -tol) || (dp > df + tol))) || + (df < 0 && ((dp > tol) || (dp < df - tol)))) + { + continue; + } + + Vector p = a.Plus((b.Minus(a)).ScaledBy(t)); + + Inter inter; + ClosestPointTo(p, &(inter.p.x), &(inter.p.y)); + inters.Add(&inter); + } + } else { + // General numerical solution by subdivision, fallback + int cnt = 0, level = 0; + AllPointsIntersectingUntrimmed(a, b, &cnt, &level, &inters, asSegment, this); + } + + // Remove duplicate intersection points + inters.ClearTags(); + int i, j; + for(i = 0; i < inters.n; i++) { + for(j = i + 1; j < inters.n; j++) { + if(inters[i].p.Equals(inters[j].p)) { + inters[j].tag = 1; + } + } + } + inters.RemoveTagged(); + + for(i = 0; i < inters.n; i++) { + Point2d puv = inters[i].p; + + // Make sure the point lies within the finite line segment + Vector pxyz = PointAt(puv.x, puv.y); + double t = (pxyz.Minus(a)).DivProjected(ba); + if(asSegment && (t > 1 - LENGTH_EPS/bam || t < LENGTH_EPS/bam)) { + continue; + } + + // And that it lies inside our trim region + Point2d dummy = { 0, 0 }; + SBspUv::Class c = (bsp) ? bsp->ClassifyPoint(puv, dummy, this) : SBspUv::Class::OUTSIDE; + if(trimmed && c == SBspUv::Class::OUTSIDE) { + continue; + } + + // It does, so generate the intersection + SInter si; + si.p = pxyz; + si.surfNormal = NormalAt(puv.x, puv.y); + si.pinter = puv; + si.srf = this; + si.onEdge = (c != SBspUv::Class::INSIDE); + l->Add(&si); + } + + inters.Clear(); +} + +void SShell::AllPointsIntersecting(Vector a, Vector b, + List *il, + bool asSegment, bool trimmed, bool inclTangent) +{ + for(SSurface &ss : surface) { + ss.AllPointsIntersecting(a, b, il, + asSegment, trimmed, inclTangent); + } +} + + + +SShell::Class SShell::ClassifyRegion(Vector edge_n, Vector inter_surf_n, + Vector edge_surf_n) const +{ + double dot = inter_surf_n.DirectionCosineWith(edge_n); + if(fabs(dot) < DOTP_TOL) { + // The edge's surface and the edge-on-face surface + // are coincident. Test the edge's surface normal + // to see if it's with same or opposite normals. + if(inter_surf_n.Dot(edge_surf_n) > 0) { + return Class::SURF_COINC_SAME; + } else { + return Class::SURF_COINC_OPP; + } + } else if(dot > 0) { + return Class::SURF_OUTSIDE; + } else { + return Class::SURF_INSIDE; + } +} + +//----------------------------------------------------------------------------- +// Does the given point lie on our shell? There are many cases; inside and +// outside are obvious, but then there's all the edge-on-edge and edge-on-face +// possibilities. +// +// To calculate, we intersect a ray through p with our shell, and classify +// using the closest intersection point. If the ray hits a surface on edge, +// then just reattempt in a different random direction. +//----------------------------------------------------------------------------- + +// table of vectors in 6 arbitrary directions covering 4 of the 8 octants. +// use overlapping sets of 3 to reduce memory usage. +static const double Random[8] = {1.278, 5.0103, 9.427, -2.331, 7.13, 2.954, 5.034, -4.777}; + +bool SShell::ClassifyEdge(Class *indir, Class *outdir, + Vector ea, Vector eb, + Vector p, + Vector edge_n_in, Vector edge_n_out, Vector surf_n) +{ + List l = {}; + + // First, check for edge-on-edge + int edge_inters = 0; + Vector inter_surf_n[2], inter_edge_n[2]; + for(SSurface &srf : surface) { + if(srf.LineEntirelyOutsideBbox(ea, eb, /*asSegment=*/true)) continue; + + SEdgeList *sel = &(srf.edges); + SEdge *se; + for(se = sel->l.First(); se; se = sel->l.NextAfter(se)) { + if((ea.Equals(se->a) && eb.Equals(se->b)) || + (eb.Equals(se->a) && ea.Equals(se->b)) || + p.OnLineSegment(se->a, se->b)) + { + if(edge_inters < 2) { + // Edge-on-edge case + Point2d pm; + srf.ClosestPointTo(p, &pm, /*mustConverge=*/false); + // A vector normal to the surface, at the intersection point + inter_surf_n[edge_inters] = srf.NormalAt(pm); + // A vector normal to the intersecting edge (but within the + // intersecting surface) at the intersection point, pointing + // out. + inter_edge_n[edge_inters] = + (inter_surf_n[edge_inters]).Cross((se->b).Minus((se->a))); + } + + edge_inters++; + } + } + } + + if(edge_inters == 2) { + //! @todo make this use the appropriate curved normals + double dotp[2]; + for(int i = 0; i < 2; i++) { + dotp[i] = edge_n_out.DirectionCosineWith(inter_surf_n[i]); + } + + if(fabs(dotp[1]) < DOTP_TOL) { + swap(dotp[0], dotp[1]); + swap(inter_surf_n[0], inter_surf_n[1]); + swap(inter_edge_n[0], inter_edge_n[1]); + } + + Class coinc = (surf_n.Dot(inter_surf_n[0])) > 0 ? Class::SURF_COINC_SAME : Class::SURF_COINC_OPP; + + if(fabs(dotp[0]) < DOTP_TOL && fabs(dotp[1]) < DOTP_TOL) { + // This is actually an edge on face case, just that the face + // is split into two pieces joining at our edge. + *indir = coinc; + *outdir = coinc; + } else if(fabs(dotp[0]) < DOTP_TOL && dotp[1] > DOTP_TOL) { + if(edge_n_out.Dot(inter_edge_n[0]) > 0) { + *indir = coinc; + *outdir = Class::SURF_OUTSIDE; + } else { + *indir = Class::SURF_INSIDE; + *outdir = coinc; + } + } else if(fabs(dotp[0]) < DOTP_TOL && dotp[1] < -DOTP_TOL) { + if(edge_n_out.Dot(inter_edge_n[0]) > 0) { + *indir = coinc; + *outdir = Class::SURF_INSIDE; + } else { + *indir = Class::SURF_OUTSIDE; + *outdir = coinc; + } + } else if(dotp[0] > DOTP_TOL && dotp[1] > DOTP_TOL) { + *indir = Class::SURF_INSIDE; + *outdir = Class::SURF_OUTSIDE; + } else if(dotp[0] < -DOTP_TOL && dotp[1] < -DOTP_TOL) { + *indir = Class::SURF_OUTSIDE; + *outdir = Class::SURF_INSIDE; + } else { + // Edge is tangent to the shell at shell's edge, so can't be + // a boundary of the surface. + return false; + } + return true; + } + + if(edge_inters != 0) dbp("bad, edge_inters=%d", edge_inters); + + // Next, check for edge-on-surface. The ray-casting for edge-inside-shell + // would catch this too, but test separately, for speed (since many edges + // are on surface) and for numerical stability, so we don't pick up + // the additional error from the line intersection. + + for(SSurface &srf : surface) { + if(srf.LineEntirelyOutsideBbox(ea, eb, /*asSegment=*/true)) continue; + + Point2d puv; + srf.ClosestPointTo(p, &(puv.x), &(puv.y), /*mustConverge=*/false); + Vector pp = srf.PointAt(puv); + + if((pp.Minus(p)).Magnitude() > LENGTH_EPS) continue; + Point2d dummy = { 0, 0 }; + SBspUv::Class c = (srf.bsp) ? srf.bsp->ClassifyPoint(puv, dummy, &srf) : SBspUv::Class::OUTSIDE; + if(c == SBspUv::Class::OUTSIDE) continue; + + // Edge-on-face (unless edge-on-edge above superseded) + Point2d pin, pout; + srf.ClosestPointTo(p.Plus(edge_n_in), &pin, /*mustConverge=*/false); + srf.ClosestPointTo(p.Plus(edge_n_out), &pout, /*mustConverge=*/false); + + Vector surf_n_in = srf.NormalAt(pin), + surf_n_out = srf.NormalAt(pout); + + *indir = ClassifyRegion(edge_n_in, surf_n_in, surf_n); + *outdir = ClassifyRegion(edge_n_out, surf_n_out, surf_n); + return true; + } + + // Edge is not on face or on edge; so it's either inside or outside + // the shell, and we'll determine which by raycasting. + int cnt = 0; + for(;;) { + // Cast a ray in a random direction (two-sided so that we test if + // the point lies on a surface, but use only one side for in/out + // testing) + Vector ray = Vector::From(Random[cnt], Random[cnt+1], Random[cnt+2]); + + AllPointsIntersecting( + p.Minus(ray), p.Plus(ray), &l, + /*asSegment=*/false, /*trimmed=*/true, /*inclTangent=*/false); + + // no intersections means it's outside + *indir = Class::SURF_OUTSIDE; + *outdir = Class::SURF_OUTSIDE; + double dmin = VERY_POSITIVE; + bool onEdge = false; + edge_inters = 0; + + SInter *si; + for(si = l.First(); si; si = l.NextAfter(si)) { + double t = ((si->p).Minus(p)).DivProjected(ray); + if(t*ray.Magnitude() < -LENGTH_EPS) { + // wrong side, doesn't count + continue; + } + + double d = ((si->p).Minus(p)).Magnitude(); + + // We actually should never hit this case; it should have been + // handled above. + if(d < LENGTH_EPS && si->onEdge) { + edge_inters++; + } + + if(d < dmin) { + dmin = d; + // Edge does not lie on surface; either strictly inside + // or strictly outside + if((si->surfNormal).Dot(ray) > 0) { + *indir = Class::SURF_INSIDE; + *outdir = Class::SURF_INSIDE; + } else { + *indir = Class::SURF_OUTSIDE; + *outdir = Class::SURF_OUTSIDE; + } + onEdge = si->onEdge; + } + } + l.Clear(); + + // If the point being tested lies exactly on an edge of the shell, + // then our ray always lies on edge, and that's okay. Otherwise + // try again in a different random direction. + if(!onEdge) break; + cnt++; + if(cnt > 5) { + dbp("can't find a ray that doesn't hit on edge!"); + dbp("on edge = %d, edge_inters = %d", onEdge, edge_inters); + SS.nakedEdges.AddEdge(ea, eb); + break; + } + } + + return true; +} + +} // namespace SolveSpace diff --git a/src/srf/shell.cpp b/src/srf/shell.cpp new file mode 100644 index 0000000..9d7fa47 --- /dev/null +++ b/src/srf/shell.cpp @@ -0,0 +1,618 @@ +//----------------------------------------------------------------------------- +// Anything involving NURBS shells (i.e., shells); except +// for the real math, which is in ratpoly.cpp. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +typedef struct { + hSCurve hc; + hSSurface hs; +} TrimLine; + + +void SShell::MakeFromExtrusionOf(SBezierLoopSet *sbls, Vector t0, Vector t1, RgbaColor color) +{ + // Make the extrusion direction consistent with respect to the normal + // of the sketch we're extruding. + if((t0.Minus(t1)).Dot(sbls->normal) < 0) { + swap(t0, t1); + } + + // Define a coordinate system to contain the original sketch, and get + // a bounding box in that csys + Vector n = sbls->normal.ScaledBy(-1); + Vector u = n.Normal(0), v = n.Normal(1); + Vector orig = sbls->point; + double umax = VERY_NEGATIVE, umin = VERY_POSITIVE; + sbls->GetBoundingProjd(u, orig, &umin, &umax); + double vmax = VERY_NEGATIVE, vmin = VERY_POSITIVE; + sbls->GetBoundingProjd(v, orig, &vmin, &vmax); + // and now fix things up so that all u and v lie between 0 and 1 + orig = orig.Plus(u.ScaledBy(umin)); + orig = orig.Plus(v.ScaledBy(vmin)); + u = u.ScaledBy(umax - umin); + v = v.ScaledBy(vmax - vmin); + + // So we can now generate the top and bottom surfaces of the extrusion, + // planes within a translated (and maybe mirrored) version of that csys. + SSurface s0, s1; + s0 = SSurface::FromPlane(orig.Plus(t0), u, v); + s0.color = color; + s1 = SSurface::FromPlane(orig.Plus(t1).Plus(u), u.ScaledBy(-1), v); + s1.color = color; + hSSurface hs0 = surface.AddAndAssignId(&s0), + hs1 = surface.AddAndAssignId(&s1); + + // Now go through the input curves. For each one, generate its surface + // of extrusion, its two translated trim curves, and one trim line. We + // go through by loops so that we can assign the lines correctly. + SBezierLoop *sbl; + for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) { + SBezier *sb; + List trimLines = {}; + + for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) { + // Generate the surface of extrusion of this curve, and add + // it to the list + SSurface ss = SSurface::FromExtrusionOf(sb, t0, t1); + ss.color = color; + hSSurface hsext = surface.AddAndAssignId(&ss); + + // Translate the curve by t0 and t1 to produce two trim curves + SCurve sc = {}; + sc.isExact = true; + sc.exact = sb->TransformedBy(t0, Quaternion::IDENTITY, 1.0); + (sc.exact).MakePwlInto(&(sc.pts)); + sc.surfA = hs0; + sc.surfB = hsext; + hSCurve hc0 = curve.AddAndAssignId(&sc); + + sc = {}; + sc.isExact = true; + sc.exact = sb->TransformedBy(t1, Quaternion::IDENTITY, 1.0); + (sc.exact).MakePwlInto(&(sc.pts)); + sc.surfA = hs1; + sc.surfB = hsext; + hSCurve hc1 = curve.AddAndAssignId(&sc); + + STrimBy stb0, stb1; + // The translated curves trim the flat top and bottom surfaces. + stb0 = STrimBy::EntireCurve(this, hc0, /*backwards=*/false); + stb1 = STrimBy::EntireCurve(this, hc1, /*backwards=*/true); + (surface.FindById(hs0))->trim.Add(&stb0); + (surface.FindById(hs1))->trim.Add(&stb1); + + // The translated curves also trim the surface of extrusion. + stb0 = STrimBy::EntireCurve(this, hc0, /*backwards=*/true); + stb1 = STrimBy::EntireCurve(this, hc1, /*backwards=*/false); + (surface.FindById(hsext))->trim.Add(&stb0); + (surface.FindById(hsext))->trim.Add(&stb1); + + // And form the trim line + Vector pt = sb->Finish(); + sc = {}; + sc.isExact = true; + sc.exact = SBezier::From(pt.Plus(t0), pt.Plus(t1)); + (sc.exact).MakePwlInto(&(sc.pts)); + hSCurve hl = curve.AddAndAssignId(&sc); + // save this for later + TrimLine tl; + tl.hc = hl; + tl.hs = hsext; + trimLines.Add(&tl); + } + + int i; + for(i = 0; i < trimLines.n; i++) { + TrimLine *tl = &(trimLines[i]); + SSurface *ss = surface.FindById(tl->hs); + + TrimLine *tlp = &(trimLines[WRAP(i-1, trimLines.n)]); + + STrimBy stb; + stb = STrimBy::EntireCurve(this, tl->hc, /*backwards=*/true); + ss->trim.Add(&stb); + stb = STrimBy::EntireCurve(this, tlp->hc, /*backwards=*/false); + ss->trim.Add(&stb); + + (curve.FindById(tl->hc))->surfA = ss->h; + (curve.FindById(tlp->hc))->surfB = ss->h; + } + trimLines.Clear(); + } +} + +bool SShell::CheckNormalAxisRelationship(SBezierLoopSet *sbls, Vector pt, Vector axis, double da, double dx) +// Check that the direction of revolution/extrusion ends up parallel to the normal of +// the sketch, on the side of the axis where the sketch is. +{ + SBezierLoop *sbl; + Vector pto; + double md = VERY_NEGATIVE; + for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) { + SBezier *sb; + for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) { + // Choose the point farthest from the axis; we'll get garbage + // if we choose a point that lies on the axis, for example. + // (And our surface will be self-intersecting if the sketch + // spans the axis, so don't worry about that.) + for(int i = 0; i <= sb->deg; i++) { + Vector p = sb->ctrl[i]; + double d = p.DistanceToLine(pt, axis); + if(d > md) { + md = d; + pto = p; + } + } + } + } + Vector ptc = pto.ClosestPointOnLine(pt, axis), + up = axis.Cross(pto.Minus(ptc)).ScaledBy(da), + vp = up.Plus(axis.ScaledBy(dx)); + + return (vp.Dot(sbls->normal) > 0); +} + +// sketch must not contain the axis of revolution as a non-construction line for helix +void SShell::MakeFromHelicalRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis, + RgbaColor color, Group *group, double angles, + double anglef, double dists, double distf) { + int i0 = surface.n; // number of pre-existing surfaces + SBezierLoop *sbl; + // for testing - hard code the axial distance, and number of sections. + // distance will need to be parameters in the future. + double dist = distf - dists; + int sections = (int)(fabs(anglef - angles) / (PI / 2) + 1); + double wedge = (anglef - angles) / sections; + int startMapping = Group::REMAP_LATHE_START, endMapping = Group::REMAP_LATHE_END; + + if(CheckNormalAxisRelationship(sbls, pt, axis, anglef-angles, distf-dists)) { + swap(angles, anglef); + swap(dists, distf); + dist = -dist; + wedge = -wedge; + swap(startMapping, endMapping); + } + + // Define a coordinate system to contain the original sketch, and get + // a bounding box in that csys + Vector n = sbls->normal.ScaledBy(-1); + Vector u = n.Normal(0), v = n.Normal(1); + Vector orig = sbls->point; + double umax = VERY_NEGATIVE, umin = VERY_POSITIVE; + sbls->GetBoundingProjd(u, orig, &umin, &umax); + double vmax = VERY_NEGATIVE, vmin = VERY_POSITIVE; + sbls->GetBoundingProjd(v, orig, &vmin, &vmax); + // and now fix things up so that all u and v lie between 0 and 1 + orig = orig.Plus(u.ScaledBy(umin)); + orig = orig.Plus(v.ScaledBy(vmin)); + u = u.ScaledBy(umax - umin); + v = v.ScaledBy(vmax - vmin); + + // So we can now generate the end caps of the extrusion within + // a translated and rotated (and maybe mirrored) version of that csys. + SSurface s0, s1; + s0 = SSurface::FromPlane(orig.RotatedAbout(pt, axis, angles).Plus(axis.ScaledBy(dists)), + u.RotatedAbout(axis, angles), v.RotatedAbout(axis, angles)); + s0.color = color; + + hEntity face0 = group->Remap(Entity::NO_ENTITY, startMapping); + s0.face = face0.v; + + s1 = SSurface::FromPlane( + orig.Plus(u).RotatedAbout(pt, axis, anglef).Plus(axis.ScaledBy(distf)), + u.ScaledBy(-1).RotatedAbout(axis, anglef), v.RotatedAbout(axis, anglef)); + s1.color = color; + + hEntity face1 = group->Remap(Entity::NO_ENTITY, endMapping); + s1.face = face1.v; + + hSSurface hs0 = surface.AddAndAssignId(&s0); + hSSurface hs1 = surface.AddAndAssignId(&s1); + + // Now we actually build and trim the swept surfaces. One loop at a time. + for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) { + int i, j; + SBezier *sb; + List> hsl = {}; + + // This is where all the NURBS are created and Remapped to the generating curve + for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) { + std::vector revs(sections); + for(j = 0; j < sections; j++) { + if((dist == 0) && sb->deg == 1 && + (sb->ctrl[0]).DistanceToLine(pt, axis) < LENGTH_EPS && + (sb->ctrl[1]).DistanceToLine(pt, axis) < LENGTH_EPS) { + // This is a line on the axis of revolution; it does + // not contribute a surface. + revs[j].v = 0; + } else { + SSurface ss = SSurface::FromRevolutionOf( + sb, pt, axis, angles + (wedge)*j, angles + (wedge) * (j + 1), + dists + j * dist / sections, dists + (j + 1) * dist / sections); + ss.color = color; + if(sb->entity != 0) { + hEntity he; + he.v = sb->entity; + hEntity hface = group->Remap(he, Group::REMAP_LINE_TO_FACE); + if(SK.entity.FindByIdNoOops(hface) != NULL) { + ss.face = hface.v; + } + } + revs[j] = surface.AddAndAssignId(&ss); + } + } + hsl.Add(&revs); + } + // Still the same loop. Need to create trim curves + for(i = 0; i < sbl->l.n; i++) { + std::vector revs = hsl[i], revsp = hsl[WRAP(i - 1, sbl->l.n)]; + + sb = &(sbl->l[i]); + + // we will need the grid t-values for this entire row of surfaces + List t_values; + t_values = {}; + if (revs[0].v) { + double ps = 0.0; + t_values.Add(&ps); + (surface.FindById(revs[0]))->MakeTriangulationGridInto( + &t_values, 0.0, 1.0, true, 0); + } + // we generate one more curve than we did surfaces + for(j = 0; j <= sections; j++) { + SCurve sc; + Quaternion qs = Quaternion::From(axis, angles + wedge * j); + // we want Q*(x - p) + p = Q*x + (p - Q*p) + Vector ts = + pt.Minus(qs.Rotate(pt)).Plus(axis.ScaledBy(dists + j * dist / sections)); + + // If this input curve generated a surface, then trim that + // surface with the rotated version of the input curve. + if(revs[0].v) { // not d[j] because crash on j==sections + sc = {}; + sc.isExact = true; + sc.exact = sb->TransformedBy(ts, qs, 1.0); + // make the PWL for the curve based on t value list + for(int x = 0; x < t_values.n; x++) { + SCurvePt scpt; + scpt.tag = 0; + scpt.p = sc.exact.PointAt(t_values[x]); + scpt.vertex = (x == 0) || (x == (t_values.n - 1)); + sc.pts.Add(&scpt); + } + + // the surfaces already exists so trim with this curve + if(j < sections) { + sc.surfA = revs[j]; + } else { + sc.surfA = hs1; // end cap + } + + if(j > 0) { + sc.surfB = revs[j - 1]; + } else { + sc.surfB = hs0; // staring cap + } + + hSCurve hcb = curve.AddAndAssignId(&sc); + + STrimBy stb; + stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true); + (surface.FindById(sc.surfA))->trim.Add(&stb); + stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false); + (surface.FindById(sc.surfB))->trim.Add(&stb); + } else if(j == 0) { // curve was on the rotation axis and is shared by the end caps. + sc = {}; + sc.isExact = true; + sc.exact = sb->TransformedBy(ts, qs, 1.0); + (sc.exact).MakePwlInto(&(sc.pts)); + sc.surfA = hs1; // end cap + sc.surfB = hs0; // staring cap + hSCurve hcb = curve.AddAndAssignId(&sc); + + STrimBy stb; + stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true); + (surface.FindById(sc.surfA))->trim.Add(&stb); + stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false); + (surface.FindById(sc.surfB))->trim.Add(&stb); + } + + // And if this input curve and the one after it both generated + // surfaces, then trim both of those by the appropriate + // curve based on the control points. + if((j < sections) && revs[j].v && revsp[j].v) { + SSurface *ss = surface.FindById(revs[j]); + + sc = {}; + sc.isExact = true; + sc.exact = SBezier::From(ss->ctrl[0][0], ss->ctrl[0][1], ss->ctrl[0][2]); + sc.exact.weight[1] = ss->weight[0][1]; + double max_dt = 0.5; + if (sc.exact.deg > 1) max_dt = 0.125; + (sc.exact).MakePwlInto(&(sc.pts), 0.0, max_dt); + sc.surfA = revs[j]; + sc.surfB = revsp[j]; + + hSCurve hcc = curve.AddAndAssignId(&sc); + + STrimBy stb; + stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/false); + (surface.FindById(sc.surfA))->trim.Add(&stb); + stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/true); + (surface.FindById(sc.surfB))->trim.Add(&stb); + } + } + t_values.Clear(); + } + + hsl.Clear(); + } + + if(dist == 0) { + MakeFirstOrderRevolvedSurfaces(pt, axis, i0); + } +} + +void SShell::MakeFromRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis, RgbaColor color, + Group *group) { + int i0 = surface.n; // number of pre-existing surfaces + SBezierLoop *sbl; + + if(CheckNormalAxisRelationship(sbls, pt, axis, 1.0, 0.0)) { + axis = axis.ScaledBy(-1); + } + + // Now we actually build and trim the surfaces. + for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) { + int i, j; + SBezier *sb; + List> hsl = {}; + + for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) { + std::vector revs(4); + for(j = 0; j < 4; j++) { + if(sb->deg == 1 && + (sb->ctrl[0]).DistanceToLine(pt, axis) < LENGTH_EPS && + (sb->ctrl[1]).DistanceToLine(pt, axis) < LENGTH_EPS) + { + // This is a line on the axis of revolution; it does + // not contribute a surface. + revs[j].v = 0; + } else { + SSurface ss = SSurface::FromRevolutionOf(sb, pt, axis, (PI / 2) * j, + (PI / 2) * (j + 1), 0.0, 0.0); + ss.color = color; + if(sb->entity != 0) { + hEntity he; + he.v = sb->entity; + hEntity hface = group->Remap(he, Group::REMAP_LINE_TO_FACE); + if(SK.entity.FindByIdNoOops(hface) != NULL) { + ss.face = hface.v; + } + } + revs[j] = surface.AddAndAssignId(&ss); + } + } + hsl.Add(&revs); + } + + for(i = 0; i < sbl->l.n; i++) { + std::vector revs = hsl[i], + revsp = hsl[WRAP(i-1, sbl->l.n)]; + + sb = &(sbl->l[i]); + + for(j = 0; j < 4; j++) { + SCurve sc; + Quaternion qs = Quaternion::From(axis, (PI/2)*j); + // we want Q*(x - p) + p = Q*x + (p - Q*p) + Vector ts = pt.Minus(qs.Rotate(pt)); + + // If this input curve generate a surface, then trim that + // surface with the rotated version of the input curve. + if(revs[j].v) { + sc = {}; + sc.isExact = true; + sc.exact = sb->TransformedBy(ts, qs, 1.0); + (sc.exact).MakePwlInto(&(sc.pts)); + sc.surfA = revs[j]; + sc.surfB = revs[WRAP(j-1, 4)]; + + hSCurve hcb = curve.AddAndAssignId(&sc); + + STrimBy stb; + stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true); + (surface.FindById(sc.surfA))->trim.Add(&stb); + stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false); + (surface.FindById(sc.surfB))->trim.Add(&stb); + } + + // And if this input curve and the one after it both generated + // surfaces, then trim both of those by the appropriate + // circle. + if(revs[j].v && revsp[j].v) { + SSurface *ss = surface.FindById(revs[j]); + + sc = {}; + sc.isExact = true; + sc.exact = SBezier::From(ss->ctrl[0][0], + ss->ctrl[0][1], + ss->ctrl[0][2]); + sc.exact.weight[1] = ss->weight[0][1]; + (sc.exact).MakePwlInto(&(sc.pts)); + sc.surfA = revs[j]; + sc.surfB = revsp[j]; + + hSCurve hcc = curve.AddAndAssignId(&sc); + + STrimBy stb; + stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/false); + (surface.FindById(sc.surfA))->trim.Add(&stb); + stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/true); + (surface.FindById(sc.surfB))->trim.Add(&stb); + } + } + } + + hsl.Clear(); + } + + MakeFirstOrderRevolvedSurfaces(pt, axis, i0); +} + +void SShell::MakeFirstOrderRevolvedSurfaces(Vector pt, Vector axis, int i0) { + int i; + + for(i = i0; i < surface.n; i++) { + SSurface *srf = &(surface[i]); + + // Revolution of a line; this is potentially a plane, which we can + // rewrite to have degree (1, 1). + if(srf->degm == 1 && srf->degn == 2) { + // close start, far start, far finish + Vector cs, fs, ff; + double d0, d1; + d0 = (srf->ctrl[0][0]).DistanceToLine(pt, axis); + d1 = (srf->ctrl[1][0]).DistanceToLine(pt, axis); + + if(d0 > d1) { + cs = srf->ctrl[1][0]; + fs = srf->ctrl[0][0]; + ff = srf->ctrl[0][2]; + } else { + cs = srf->ctrl[0][0]; + fs = srf->ctrl[1][0]; + ff = srf->ctrl[1][2]; + } + + // origin close, origin far + Vector oc = cs.ClosestPointOnLine(pt, axis), + of = fs.ClosestPointOnLine(pt, axis); + + if(oc.Equals(of)) { + // This is a plane, not a (non-degenerate) cone. + Vector oldn = srf->NormalAt(0.5, 0.5); + + Vector u = fs.Minus(of), v; + + v = (axis.Cross(u)).WithMagnitude(1); + + double vm = (ff.Minus(of)).Dot(v); + v = v.ScaledBy(vm); + + srf->degm = 1; + srf->degn = 1; + srf->ctrl[0][0] = of; + srf->ctrl[0][1] = of.Plus(u); + srf->ctrl[1][0] = of.Plus(v); + srf->ctrl[1][1] = of.Plus(u).Plus(v); + srf->weight[0][0] = 1; + srf->weight[0][1] = 1; + srf->weight[1][0] = 1; + srf->weight[1][1] = 1; + + if(oldn.Dot(srf->NormalAt(0.5, 0.5)) < 0) { + swap(srf->ctrl[0][0], srf->ctrl[1][0]); + swap(srf->ctrl[0][1], srf->ctrl[1][1]); + } + continue; + } + + if(fabs(d0 - d1) < LENGTH_EPS) { + // This is a cylinder; so transpose it so that we'll recognize + // it as a surface of extrusion. + SSurface sn = *srf; + + // Transposing u and v flips the normal, so reverse u to + // flip it again and put it back where we started. + sn.degm = 2; + sn.degn = 1; + int dm, dn; + for(dm = 0; dm <= 1; dm++) { + for(dn = 0; dn <= 2; dn++) { + sn.ctrl [dn][dm] = srf->ctrl [1-dm][dn]; + sn.weight[dn][dm] = srf->weight[1-dm][dn]; + } + } + + *srf = sn; + continue; + } + } + } +} + +void SShell::MakeFromCopyOf(SShell *a) { + ssassert(this != a, "Can't make from copy of self"); + MakeFromTransformationOf(a, + Vector::From(0, 0, 0), Quaternion::IDENTITY, 1.0); +} + +void SShell::MakeFromTransformationOf(SShell *a, + Vector t, Quaternion q, double scale) +{ + booleanFailed = false; + surface.ReserveMore(a->surface.n); + for(SSurface &s : a->surface) { + SSurface n; + n = SSurface::FromTransformationOf(&s, t, q, scale, /*includingTrims=*/true); + surface.Add(&n); // keeping the old ID + } + + curve.ReserveMore(a->curve.n); + for(SCurve &c : a->curve) { + SCurve n; + n = SCurve::FromTransformationOf(&c, t, q, scale); + curve.Add(&n); // keeping the old ID + } +} + +void SShell::MakeEdgesInto(SEdgeList *sel) { + for(SSurface &s : surface) { + s.MakeEdgesInto(this, sel, SSurface::MakeAs::XYZ); + } +} + +void SShell::MakeSectionEdgesInto(Vector n, double d, SEdgeList *sel, SBezierList *sbl) +{ + for(SSurface &s : surface) { + if(s.CoincidentWithPlane(n, d)) { + s.MakeSectionEdgesInto(this, sel, sbl); + } + } +} + +void SShell::TriangulateInto(SMesh *sm) { +#pragma omp parallel for + for(int i=0; iTriangulateInto(this, &m); + #pragma omp critical + sm->MakeFromCopyOf(&m); + m.Clear(); + } +} + +bool SShell::IsEmpty() const { + return surface.IsEmpty(); +} + +void SShell::Clear() { + for(SSurface &s : surface) { + s.Clear(); + } + surface.Clear(); + + for(SCurve &c : curve) { + c.Clear(); + } + curve.Clear(); +} + +} // namespace SolveSpace diff --git a/src/srf/surface.cpp b/src/srf/surface.cpp new file mode 100644 index 0000000..8ddd084 --- /dev/null +++ b/src/srf/surface.cpp @@ -0,0 +1,494 @@ +//----------------------------------------------------------------------------- +// Anything involving surfaces and sets of surfaces (i.e., shells); except +// for the real math, which is in ratpoly.cpp. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +SSurface SSurface::FromExtrusionOf(SBezier *sb, Vector t0, Vector t1) { + SSurface ret = {}; + + ret.degm = sb->deg; + ret.degn = 1; + + int i; + for(i = 0; i <= ret.degm; i++) { + ret.ctrl[i][0] = (sb->ctrl[i]).Plus(t0); + ret.weight[i][0] = sb->weight[i]; + + ret.ctrl[i][1] = (sb->ctrl[i]).Plus(t1); + ret.weight[i][1] = sb->weight[i]; + } + + return ret; +} + +bool SSurface::IsExtrusion(SBezier *of, Vector *alongp) const { + int i; + + if(degn != 1) return false; + + Vector along = (ctrl[0][1]).Minus(ctrl[0][0]); + for(i = 0; i <= degm; i++) { + if((fabs(weight[i][1] - weight[i][0]) < LENGTH_EPS) && + ((ctrl[i][1]).Minus(ctrl[i][0])).Equals(along)) + { + continue; + } + return false; + } + + // yes, we are a surface of extrusion; copy the original curve and return + if(of) { + for(i = 0; i <= degm; i++) { + of->weight[i] = weight[i][0]; + of->ctrl[i] = ctrl[i][0]; + } + of->deg = degm; + *alongp = along; + } + return true; +} + +bool SSurface::IsCylinder(Vector *axis, Vector *center, double *r, + Vector *start, Vector *finish) const +{ + SBezier sb; + if(!IsExtrusion(&sb, axis)) return false; + if(!sb.IsCircle(*axis, center, r)) return false; + + *start = sb.ctrl[0]; + *finish = sb.ctrl[2]; + return true; +} + +// Create a surface patch by revolving and possibly translating a curve. +// Works for sections up to but not including 180 degrees. +SSurface SSurface::FromRevolutionOf(SBezier *sb, Vector pt, Vector axis, double thetas, + double thetaf, double dists, + double distf) { // s is start, f is finish + SSurface ret = {}; + ret.degm = sb->deg; + ret.degn = 2; + + double dtheta = fabs(WRAP_SYMMETRIC(thetaf - thetas, 2*PI)); + double w = cos(dtheta / 2); + + // Revolve the curve about the z axis + int i; + for(i = 0; i <= ret.degm; i++) { + Vector p = sb->ctrl[i]; + + Vector ps = p.RotatedAbout(pt, axis, thetas), + pf = p.RotatedAbout(pt, axis, thetaf); + + // The middle control point should be at the intersection of the tangents at ps and pf. + // This is equivalent but works for 0 <= angle < 180 degrees. + Vector mid = ps.Plus(pf).ScaledBy(0.5); + Vector c = ps.ClosestPointOnLine(pt, axis); + Vector ct = mid.Minus(c).ScaledBy(1 / (w * w)).Plus(c); + + // not sure this is needed + if(ps.Equals(pf)) { + ps = c; + ct = c; + pf = c; + } + // moving along the axis can create hilical surfaces (or straight extrusion if + // thetas==thetaf) + ret.ctrl[i][0] = ps.Plus(axis.ScaledBy(dists)); + ret.ctrl[i][1] = ct.Plus(axis.ScaledBy((dists + distf) / 2)); + ret.ctrl[i][2] = pf.Plus(axis.ScaledBy(distf)); + + ret.weight[i][0] = sb->weight[i]; + ret.weight[i][1] = sb->weight[i] * w; + ret.weight[i][2] = sb->weight[i]; + } + + return ret; +} + +SSurface SSurface::FromPlane(Vector pt, Vector u, Vector v) { + SSurface ret = {}; + + ret.degm = 1; + ret.degn = 1; + + ret.weight[0][0] = ret.weight[0][1] = 1; + ret.weight[1][0] = ret.weight[1][1] = 1; + + ret.ctrl[0][0] = pt; + ret.ctrl[0][1] = pt.Plus(u); + ret.ctrl[1][0] = pt.Plus(v); + ret.ctrl[1][1] = pt.Plus(v).Plus(u); + + return ret; +} + +SSurface SSurface::FromTransformationOf(SSurface *a, Vector t, Quaternion q, double scale, + bool includingTrims) +{ + bool needRotate = !EXACT(q.vx == 0.0 && q.vy == 0.0 && q.vz == 0.0 && q.w == 1.0); + bool needTranslate = !EXACT(t.x == 0.0 && t.y == 0.0 && t.z == 0.0); + bool needScale = !EXACT(scale == 1.0); + + SSurface ret = {}; + ret.h = a->h; + ret.color = a->color; + ret.face = a->face; + + ret.degm = a->degm; + ret.degn = a->degn; + int i, j; + for(i = 0; i <= 3; i++) { + for(j = 0; j <= 3; j++) { + Vector ctrl = a->ctrl[i][j]; + if(needScale) { + ctrl = ctrl.ScaledBy(scale); + } + if(needRotate) { + ctrl = q.Rotate(ctrl); + } + if(needTranslate) { + ctrl = ctrl.Plus(t); + } + ret.ctrl[i][j] = ctrl; + ret.weight[i][j] = a->weight[i][j]; + } + } + + if(includingTrims) { + STrimBy *stb; + ret.trim.ReserveMore(a->trim.n); + for(stb = a->trim.First(); stb; stb = a->trim.NextAfter(stb)) { + STrimBy n = *stb; + if(needScale) { + n.start = n.start.ScaledBy(scale); + n.finish = n.finish.ScaledBy(scale); + } + if(needRotate) { + n.start = q.Rotate(n.start); + n.finish = q.Rotate(n.finish); + } + if(needTranslate) { + n.start = n.start.Plus(t); + n.finish = n.finish.Plus(t); + } + ret.trim.Add(&n); + } + } + + if(scale < 0) { + // If we mirror every surface of a shell, then it will end up inside + // out. So fix that here. + ret.Reverse(); + } + + return ret; +} + +void SSurface::GetAxisAlignedBounding(Vector *ptMax, Vector *ptMin) const { + *ptMax = Vector::From(VERY_NEGATIVE, VERY_NEGATIVE, VERY_NEGATIVE); + *ptMin = Vector::From(VERY_POSITIVE, VERY_POSITIVE, VERY_POSITIVE); + + int i, j; + for(i = 0; i <= degm; i++) { + for(j = 0; j <= degn; j++) { + (ctrl[i][j]).MakeMaxMin(ptMax, ptMin); + } + } +} + +bool SSurface::LineEntirelyOutsideBbox(Vector a, Vector b, bool asSegment) const { + Vector amax, amin; + GetAxisAlignedBounding(&amax, &amin); + if(!Vector::BoundingBoxIntersectsLine(amax, amin, a, b, asSegment)) { + // The line segment could fail to intersect the bbox, but lie entirely + // within it and intersect the surface. + if(a.OutsideAndNotOn(amax, amin) && b.OutsideAndNotOn(amax, amin)) { + return true; + } + } + return false; +} + +//----------------------------------------------------------------------------- +// Generate the piecewise linear approximation of the trim stb, which applies +// to the curve sc. +//----------------------------------------------------------------------------- +void SSurface::MakeTrimEdgesInto(SEdgeList *sel, MakeAs flags, + SCurve *sc, STrimBy *stb) +{ + Vector prev = Vector::From(0, 0, 0); + bool inCurve = false, empty = true; + double u = 0, v = 0; + + int i, first, last, increment; + if(stb->backwards) { + first = sc->pts.n - 1; + last = 0; + increment = -1; + } else { + first = 0; + last = sc->pts.n - 1; + increment = 1; + } + for(i = first; i != (last + increment); i += increment) { + Vector tpt, *pt = &(sc->pts[i].p); + + if(flags == MakeAs::UV) { + ClosestPointTo(*pt, &u, &v); + tpt = Vector::From(u, v, 0); + } else { + tpt = *pt; + } + + if(inCurve) { + sel->AddEdge(prev, tpt, sc->h.v, stb->backwards); + empty = false; + } + + prev = tpt; // either uv or xyz, depending on flags + + if(pt->Equals(stb->start)) inCurve = true; + if(pt->Equals(stb->finish)) inCurve = false; + } + if(inCurve) dbp("trim was unterminated"); + if(empty) dbp("trim was empty"); +} + +//----------------------------------------------------------------------------- +// Generate all of our trim curves, in piecewise linear form. We can do +// so in either uv or xyz coordinates. And if requested, then we can use +// the split curves from useCurvesFrom instead of the curves in our own +// shell. +//----------------------------------------------------------------------------- +void SSurface::MakeEdgesInto(SShell *shell, SEdgeList *sel, MakeAs flags, + SShell *useCurvesFrom) +{ + STrimBy *stb; + for(stb = trim.First(); stb; stb = trim.NextAfter(stb)) { + SCurve *sc = shell->curve.FindById(stb->curve); + + // We have the option to use the curves from another shell; this + // is relevant when generating the coincident edges while doing the + // Booleans, since the curves from the output shell will be split + // against any intersecting surfaces (and the originals aren't). + if(useCurvesFrom) { + sc = useCurvesFrom->curve.FindById(sc->newH); + } + + MakeTrimEdgesInto(sel, flags, sc, stb); + } +} + +//----------------------------------------------------------------------------- +// Compute the exact tangent to the intersection curve between two surfaces, +// by taking the cross product of the surface normals. We choose the direction +// of this tangent so that its dot product with dir is positive. +//----------------------------------------------------------------------------- +Vector SSurface::ExactSurfaceTangentAt(Vector p, SSurface *srfA, SSurface *srfB, Vector dir) +{ + Point2d puva, puvb; + srfA->ClosestPointTo(p, &puva); + srfB->ClosestPointTo(p, &puvb); + Vector ts = (srfA->NormalAt(puva)).Cross( + (srfB->NormalAt(puvb))); + ts = ts.WithMagnitude(1); + if(ts.Dot(dir) < 0) { + ts = ts.ScaledBy(-1); + } + return ts; +} + +//----------------------------------------------------------------------------- +// Report our trim curves. If a trim curve is exact and sbl is not null, then +// add its exact form to sbl. Otherwise, add its piecewise linearization to +// sel. +//----------------------------------------------------------------------------- +void SSurface::MakeSectionEdgesInto(SShell *shell, SEdgeList *sel, SBezierList *sbl) +{ + STrimBy *stb; + for(stb = trim.First(); stb; stb = trim.NextAfter(stb)) { + SCurve *sc = shell->curve.FindById(stb->curve); + SBezier *sb = &(sc->exact); + + if(sbl && sc->isExact && (sb->deg != 1 || !sel)) { + double ts, tf; + if(stb->backwards) { + sb->ClosestPointTo(stb->start, &tf); + sb->ClosestPointTo(stb->finish, &ts); + } else { + sb->ClosestPointTo(stb->start, &ts); + sb->ClosestPointTo(stb->finish, &tf); + } + SBezier junk_bef, keep_aft; + sb->SplitAt(ts, &junk_bef, &keep_aft); + // In the kept piece, the range that used to go from ts to 1 + // now goes from 0 to 1; so rescale tf appropriately. + tf = (tf - ts)/(1 - ts); + + SBezier keep_bef, junk_aft; + keep_aft.SplitAt(tf, &keep_bef, &junk_aft); + + sbl->l.Add(&keep_bef); + } else if(sbl && !sel && !sc->isExact) { + // We must approximate this trim curve, as piecewise cubic sections. + SSurface *srfA = shell->surface.FindById(sc->surfA); + SSurface *srfB = shell->surface.FindById(sc->surfB); + + Vector s = stb->backwards ? stb->finish : stb->start, + f = stb->backwards ? stb->start : stb->finish; + + int sp, fp; + for(sp = 0; sp < sc->pts.n; sp++) { + if(s.Equals(sc->pts[sp].p)) break; + } + if(sp >= sc->pts.n) return; + for(fp = sp; fp < sc->pts.n; fp++) { + if(f.Equals(sc->pts[fp].p)) break; + } + if(fp >= sc->pts.n) return; + // So now the curve we want goes from elem[sp] to elem[fp] + + while(sp < fp) { + // Initially, we'll try approximating the entire trim curve + // as a single Bezier segment + int fpt = fp; + + for(;;) { + // So construct a cubic Bezier with the correct endpoints + // and tangents for the current span. + Vector st = sc->pts[sp].p, + ft = sc->pts[fpt].p, + sf = ft.Minus(st); + double m = sf.Magnitude() / 3; + + Vector stan = ExactSurfaceTangentAt(st, srfA, srfB, sf), + ftan = ExactSurfaceTangentAt(ft, srfA, srfB, sf); + + SBezier sb = SBezier::From(st, + st.Plus (stan.WithMagnitude(m)), + ft.Minus(ftan.WithMagnitude(m)), + ft); + + // And test how much this curve deviates from the + // intermediate points (if any). + int i; + bool tooFar = false; + for(i = sp + 1; i <= (fpt - 1); i++) { + Vector p = sc->pts[i].p; + double t; + sb.ClosestPointTo(p, &t, /*mustConverge=*/false); + Vector pp = sb.PointAt(t); + if((pp.Minus(p)).Magnitude() > SS.ChordTolMm()/2) { + tooFar = true; + break; + } + } + + if(tooFar) { + // Deviates by too much, so try a shorter span + fpt--; + continue; + } else { + // Okay, so use this piece and break. + sbl->l.Add(&sb); + break; + } + } + + // And continue interpolating, starting wherever the curve + // we just generated finishes. + sp = fpt; + } + } else { + if(sel) MakeTrimEdgesInto(sel, MakeAs::XYZ, sc, stb); + } + } +} + +void SSurface::TriangulateInto(SShell *shell, SMesh *sm) { + SEdgeList el = {}; + + MakeEdgesInto(shell, &el, MakeAs::UV); + + SPolygon poly = {}; + if(el.AssemblePolygon(&poly, NULL, /*keepDir=*/true)) { + int i, start = sm->l.n; + if(degm == 1 && degn == 1) { + // A surface with curvature along one direction only; so + // choose the triangulation with chords that lie as much + // as possible within the surface. And since the trim curves + // have been pwl'd to within the desired chord tol, that will + // produce a surface good to within roughly that tol. + // + // If this is just a plane (degree (1, 1)) then the triangulation + // code will notice that, and not bother checking chord tols. + poly.UvTriangulateInto(sm, this); + } else { + // A surface with compound curvature. So we must overlay a + // two-dimensional grid, and triangulate around that. + poly.UvGridTriangulateInto(sm, this); + } + + STriMeta meta = { face, color }; + for(i = start; i < sm->l.n; i++) { + STriangle *st = &(sm->l[i]); + st->meta = meta; + st->an = NormalAt(st->a.x, st->a.y); + st->bn = NormalAt(st->b.x, st->b.y); + st->cn = NormalAt(st->c.x, st->c.y); + st->a = PointAt(st->a.x, st->a.y); + st->b = PointAt(st->b.x, st->b.y); + st->c = PointAt(st->c.x, st->c.y); + // Works out that my chosen contour direction is inconsistent with + // the triangle direction, sigh. + st->FlipNormal(); + } + } else { + dbp("failed to assemble polygon to trim nurbs surface in uv space"); + } + + el.Clear(); + poly.Clear(); +} + +//----------------------------------------------------------------------------- +// Reverse the parametrisation of one of our dimensions, which flips the +// normal. We therefore must reverse all our trim curves too. The uv +// coordinates change, but trim curves are stored as xyz so nothing happens +//----------------------------------------------------------------------------- +void SSurface::Reverse() { + int i, j; + for(i = 0; i < (degm+1)/2; i++) { + for(j = 0; j <= degn; j++) { + swap(ctrl[i][j], ctrl[degm-i][j]); + swap(weight[i][j], weight[degm-i][j]); + } + } + + STrimBy *stb; + for(stb = trim.First(); stb; stb = trim.NextAfter(stb)) { + stb->backwards = !stb->backwards; + swap(stb->start, stb->finish); + } +} + +void SSurface::ScaleSelfBy(double s) { + int i, j; + for(i = 0; i <= degm; i++) { + for(j = 0; j <= degn; j++) { + ctrl[i][j] = ctrl[i][j].ScaledBy(s); + } + } +} + +void SSurface::Clear() { + trim.Clear(); +} + +} // namespace SolveSpace diff --git a/src/srf/surface.h b/src/srf/surface.h new file mode 100644 index 0000000..d117167 --- /dev/null +++ b/src/srf/surface.h @@ -0,0 +1,449 @@ +//----------------------------------------------------------------------------- +// Functions relating to rational polynomial surfaces, which are trimmed by +// curves (either rational polynomial curves, or piecewise linear +// approximations to curves of intersection that can't be represented +// exactly in ratpoly form), and assembled into watertight shells. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- + +#ifndef SOLVESPACE_SURFACE_H +#define SOLVESPACE_SURFACE_H + +#include + +#include "dsc.h" +#include "handle.h" +#include "polygon.h" + +namespace SolveSpace { + +class Group; +class SBezierList; +class SSurface; +class SCurvePt; + +// Utility data structure, a two-dimensional BSP to accelerate polygon +// operations. +class SBspUv { +public: + Point2d a, b; + + SBspUv *pos; + SBspUv *neg; + + SBspUv *more; + + enum class Class : uint32_t { + INSIDE = 100, + OUTSIDE = 200, + EDGE_PARALLEL = 300, + EDGE_ANTIPARALLEL = 400, + EDGE_OTHER = 500 + }; + + static SBspUv *Alloc(); + static SBspUv *From(SEdgeList *el, SSurface *srf); + + void ScalePoints(Point2d *pt, Point2d *a, Point2d *b, SSurface *srf) const; + double ScaledSignedDistanceToLine(Point2d pt, Point2d a, Point2d b, + SSurface *srf) const; + double ScaledDistanceToLine(Point2d pt, Point2d a, Point2d b, bool asSegment, + SSurface *srf) const; + + void InsertEdge(Point2d a, Point2d b, SSurface *srf); + static SBspUv *InsertOrCreateEdge(SBspUv *where, Point2d ea, Point2d eb, SSurface *srf); + Class ClassifyPoint(Point2d p, Point2d eb, SSurface *srf) const; + Class ClassifyEdge(Point2d ea, Point2d eb, SSurface *srf) const; + double MinimumDistanceToEdge(Point2d p, SSurface *srf) const; +}; + +// Now the data structures to represent a shell of trimmed rational polynomial +// surfaces. + +class SShell; + +class hSSurface { +public: + uint32_t v; +}; + +template<> +struct IsHandleOracle : std::true_type {}; + +class hSCurve { +public: + uint32_t v; +}; + +template<> +struct IsHandleOracle : std::true_type {}; + +// Stuff for rational polynomial curves, of degree one to three. These are +// our inputs, and are also calculated for certain exact surface-surface +// intersections. +class SBezier { +public: + int tag; + int auxA, auxB; + + int deg; + Vector ctrl[4]; + double weight[4]; + uint32_t entity; + + Vector PointAt(double t) const; + Vector TangentAt(double t) const; + void ClosestPointTo(Vector p, double *t, bool mustConverge=true) const; + void SplitAt(double t, SBezier *bef, SBezier *aft) const; + bool PointOnThisAndCurve(const SBezier *sbb, Vector *p) const; + + Vector Start() const; + Vector Finish() const; + bool Equals(SBezier *b) const; + void MakePwlInto(SEdgeList *sel, double chordTol=0, double max_dt=0.0) const; + void MakePwlInto(List *l, double chordTol=0, double max_dt=0.0) const; + void MakePwlInto(SContour *sc, double chordTol=0, double max_dt=0.0) const; + void MakePwlInto(List *l, double chordTol=0, double max_dt=0.0) const; + void MakePwlWorker(List *l, double ta, double tb, double chordTol, double max_dt) const; + void MakePwlInitialWorker(List *l, double ta, double tb, double chordTol, double max_dt) const; + void MakeNonrationalCubicInto(SBezierList *bl, double tolerance, int depth = 0) const; + + void AllIntersectionsWith(const SBezier *sbb, SPointList *spl) const; + void GetBoundingProjd(Vector u, Vector orig, double *umin, double *umax) const; + void Reverse(); + + bool IsInPlane(Vector n, double d) const; + bool IsCircle(Vector axis, Vector *center, double *r) const; + bool IsRational() const; + + SBezier TransformedBy(Vector t, Quaternion q, double scale) const; + SBezier InPerspective(Vector u, Vector v, Vector n, + Vector origin, double cameraTan) const; + void ScaleSelfBy(double s); + + static SBezier From(Vector p0, Vector p1, Vector p2, Vector p3); + static SBezier From(Vector p0, Vector p1, Vector p2); + static SBezier From(Vector p0, Vector p1); + static SBezier From(Vector4 p0, Vector4 p1, Vector4 p2, Vector4 p3); + static SBezier From(Vector4 p0, Vector4 p1, Vector4 p2); + static SBezier From(Vector4 p0, Vector4 p1); +}; + +class SBezierList { +public: + List l; + + void Clear(); + void ScaleSelfBy(double s); + void CullIdenticalBeziers(bool both=true); + void AllIntersectionsWith(SBezierList *sblb, SPointList *spl) const; + bool GetPlaneContainingBeziers(Vector *p, Vector *u, Vector *v, + Vector *notCoplanarAt) const; +}; + +class SBezierLoop { +public: + int tag; + List l; + + inline void Clear() { l.Clear(); } + bool IsClosed() const; + void Reverse(); + void MakePwlInto(SContour *sc, double chordTol=0) const; + void GetBoundingProjd(Vector u, Vector orig, double *umin, double *umax) const; + + static SBezierLoop FromCurves(SBezierList *spcl, + bool *allClosed, SEdge *errorAt); +}; + +class SBezierLoopSet { +public: + List l; + Vector normal; + Vector point; + double area; + + static SBezierLoopSet From(SBezierList *spcl, SPolygon *poly, + double chordTol, + bool *allClosed, SEdge *errorAt, + SBezierLoopSet *openContours); + + void GetBoundingProjd(Vector u, Vector orig, double *umin, double *umax) const; + double SignedArea(); + void MakePwlInto(SPolygon *sp) const; + void Clear(); +}; + +class SBezierLoopSetSet { +public: + List l; + + void FindOuterFacesFrom(SBezierList *sbl, SPolygon *spxyz, SSurface *srfuv, + double chordTol, + bool *allClosed, SEdge *notClosedAt, + bool *allCoplanar, Vector *notCoplanarAt, + SBezierLoopSet *openContours); + void AddOpenPath(SBezier *sb); + void Clear(); +}; + +// Stuff for the surface trim curves: piecewise linear +class SCurvePt { +public: + int tag; + Vector p; + bool vertex; +}; + +class SCurve { +public: + hSCurve h; + + // In a Boolean, C = A op B. The curves in A and B get copied into C, and + // therefore must get new hSCurves assigned. For the curves in A and B, + // we use newH to record their new handle in C. + hSCurve newH; + enum class Source : uint32_t { + A = 100, + B = 200, + INTERSECTION = 300 + }; + Source source; + + bool isExact; + SBezier exact; + + List pts; + + hSSurface surfA; + hSSurface surfB; + + static SCurve FromTransformationOf(SCurve *a, Vector t, + Quaternion q, double scale); + SCurve MakeCopySplitAgainst(SShell *agnstA, SShell *agnstB, + SSurface *srfA, SSurface *srfB) const; + void RemoveShortSegments(SSurface *srfA, SSurface *srfB); + SSurface *GetSurfaceA(SShell *a, SShell *b) const; + SSurface *GetSurfaceB(SShell *a, SShell *b) const; + + void Clear(); + void GetAxisAlignedBounding(Vector *ptMax, Vector *ptMin) const; +}; + +// A segment of a curve by which a surface is trimmed: indicates which curve, +// by its handle, and the starting and ending points of our segment of it. +// The vector out points out of the surface; it, the surface outer normal, +// and a tangent to the beginning of the curve are all orthogonal. +class STrimBy { +public: + hSCurve curve; + bool backwards; + // If a trim runs backwards, then start and finish still correspond to + // the actual start and finish, but they appear in reverse order in + // the referenced curve. + Vector start; + Vector finish; + + static STrimBy EntireCurve(SShell *shell, hSCurve hsc, bool backwards); +}; + +// An intersection point between a line and a surface +class SInter { +public: + int tag; + Vector p; + SSurface *srf; + Point2d pinter; + Vector surfNormal; // of the intersecting surface, at pinter + bool onEdge; // pinter is on edge of trim poly +}; + +// A rational polynomial surface in Bezier form. +class SSurface { +public: + + enum class CombineAs : uint32_t { + UNION = 10, + DIFFERENCE = 11, + INTERSECTION = 12 + }; + + int tag; + hSSurface h; + + // Same as newH for the curves; record what a surface gets renamed to + // when I copy things over. + hSSurface newH; + + RgbaColor color; + uint32_t face; + + int degm, degn; + Vector ctrl[4][4]; + double weight[4][4]; + + List trim; + + // For testing whether a point (u, v) on the surface lies inside the trim + SBspUv *bsp; + SEdgeList edges; + + // For caching our initial (u, v) when doing Newton iterations to project + // a point into our surface. + Point2d cached; + + static SSurface FromExtrusionOf(SBezier *spc, Vector t0, Vector t1); + static SSurface FromRevolutionOf(SBezier *sb, Vector pt, Vector axis, double thetas, + double thetaf, double dists, double distf); + static SSurface FromPlane(Vector pt, Vector u, Vector v); + static SSurface FromTransformationOf(SSurface *a, Vector t, Quaternion q, + double scale, + bool includingTrims); + void ScaleSelfBy(double s); + + void EdgeNormalsWithinSurface(Point2d auv, Point2d buv, + Vector *pt, Vector *enin, Vector *enout, + Vector *surfn, + uint32_t auxA, + SShell *shell, SShell *sha, SShell *shb); + void FindChainAvoiding(SEdgeList *src, SEdgeList *dest, SPointList *avoid); + SSurface MakeCopyTrimAgainst(SShell *parent, SShell *a, SShell *b, + SShell *into, SSurface::CombineAs type, int dbg_index); + void TrimFromEdgeList(SEdgeList *el, bool asUv); + void IntersectAgainst(SSurface *b, SShell *agnstA, SShell *agnstB, + SShell *into); + void AddExactIntersectionCurve(SBezier *sb, SSurface *srfB, + SShell *agnstA, SShell *agnstB, SShell *into); + + typedef struct { + int tag; + Point2d p; + } Inter; + void WeightControlPoints(); + void UnWeightControlPoints(); + void CopyRowOrCol(bool row, int this_ij, SSurface *src, int src_ij); + void BlendRowOrCol(bool row, int this_ij, SSurface *a, int a_ij, + SSurface *b, int b_ij); + double DepartureFromCoplanar() const; + void SplitInHalf(bool byU, SSurface *sa, SSurface *sb); + void AllPointsIntersecting(Vector a, Vector b, + List *l, + bool asSegment, bool trimmed, bool inclTangent); + void AllPointsIntersectingUntrimmed(Vector a, Vector b, + int *cnt, int *level, + List *l, bool asSegment, + SSurface *sorig); + + void ClosestPointTo(Vector p, Point2d *puv, bool mustConverge=true); + void ClosestPointTo(Vector p, double *u, double *v, bool mustConverge=true); + bool ClosestPointNewton(Vector p, double *u, double *v, bool mustConverge=true) const; + + bool PointIntersectingLine(Vector p0, Vector p1, double *u, double *v) const; + Vector ClosestPointOnThisAndSurface(SSurface *srf2, Vector p); + void PointOnSurfaces(SSurface *s1, SSurface *s2, double *u, double *v); + void PointOnCurve(const SBezier *curve, double *up, double *vp); + Vector PointAt(double u, double v) const; + Vector PointAt(Point2d puv) const; + void TangentsAt(double u, double v, Vector *tu, Vector *tv, bool retry=true) const; + Vector NormalAt(Point2d puv) const; + Vector NormalAt(double u, double v) const; + bool LineEntirelyOutsideBbox(Vector a, Vector b, bool asSegment) const; + void GetAxisAlignedBounding(Vector *ptMax, Vector *ptMin) const; + bool CoincidentWithPlane(Vector n, double d) const; + bool CoincidentWith(SSurface *ss, bool sameNormal) const; + bool ContainsPlaneCurve(SCurve *sc) const; + bool IsExtrusion(SBezier *of, Vector *along) const; + bool IsCylinder(Vector *axis, Vector *center, double *r, + Vector *start, Vector *finish) const; + + void TriangulateInto(SShell *shell, SMesh *sm); + + // these are intended as bitmasks, even though there's just one now + enum class MakeAs : uint32_t { + UV = 0x01, + XYZ = 0x00 + }; + void MakeTrimEdgesInto(SEdgeList *sel, MakeAs flags, SCurve *sc, STrimBy *stb); + void MakeEdgesInto(SShell *shell, SEdgeList *sel, MakeAs flags, + SShell *useCurvesFrom=NULL); + + Vector ExactSurfaceTangentAt(Vector p, SSurface *srfA, SSurface *srfB, + Vector dir); + void MakeSectionEdgesInto(SShell *shell, SEdgeList *sel, SBezierList *sbl); + void MakeClassifyingBsp(SShell *shell, SShell *useCurvesFrom); + double ChordToleranceForEdge(Vector a, Vector b) const; + void MakeTriangulationGridInto(List *l, double vs, double vf, + bool swapped, int depth) const; + Vector PointAtMaybeSwapped(double u, double v, bool swapped) const; + Vector NormalAtMaybeSwapped(double u, double v, bool swapped) const; + + void Reverse(); + void Clear(); +}; + +class SShell { +public: + IdList curve; + IdList surface; + + bool booleanFailed; + + void MakeFromExtrusionOf(SBezierLoopSet *sbls, Vector t0, Vector t1, + RgbaColor color); + bool CheckNormalAxisRelationship(SBezierLoopSet *sbls, Vector pt, Vector axis, double da, double dx); + void MakeFromRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis, + RgbaColor color, Group *group); + void MakeFromHelicalRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis, RgbaColor color, + Group *group, double angles, double anglef, double dists, double distf); + void MakeFirstOrderRevolvedSurfaces(Vector pt, Vector axis, int i0); + void MakeFromUnionOf(SShell *a, SShell *b); + void MakeFromDifferenceOf(SShell *a, SShell *b); + void MakeFromIntersectionOf(SShell *a, SShell *b); + void MakeFromBoolean(SShell *a, SShell *b, SSurface::CombineAs type); + void CopyCurvesSplitAgainst(bool opA, SShell *agnst, SShell *into); + void CopySurfacesTrimAgainst(SShell *sha, SShell *shb, SShell *into, SSurface::CombineAs type); + void MakeIntersectionCurvesAgainst(SShell *against, SShell *into); + void MakeClassifyingBsps(SShell *useCurvesFrom); + void AllPointsIntersecting(Vector a, Vector b, List *il, + bool asSegment, bool trimmed, bool inclTangent); + void MakeCoincidentEdgesInto(SSurface *proto, bool sameNormal, + SEdgeList *el, SShell *useCurvesFrom); + void RewriteSurfaceHandlesForCurves(SShell *a, SShell *b); + void CleanupAfterBoolean(); + + // Definitions when classifying regions of a surface; it is either inside, + // outside, or coincident (with parallel or antiparallel normal) with a + // shell. + enum class Class : uint32_t { + SURF_INSIDE = 100, + SURF_OUTSIDE = 200, + SURF_COINC_SAME = 300, + SURF_COINC_OPP = 400 + }; + static const double DOTP_TOL; + Class ClassifyRegion(Vector edge_n, Vector inter_surf_n, + Vector edge_surf_n) const; + + bool ClassifyEdge(Class *indir, Class *outdir, + Vector ea, Vector eb, + Vector p, Vector edge_n_in, + Vector edge_n_out, Vector surf_n); + + void MakeFromCopyOf(SShell *a); + void MakeFromTransformationOf(SShell *a, + Vector trans, Quaternion q, double scale); + void MakeFromAssemblyOf(SShell *a, SShell *b); + void MergeCoincidentSurfaces(); + + void TriangulateInto(SMesh *sm); + void MakeEdgesInto(SEdgeList *sel); + void MakeSectionEdgesInto(Vector n, double d, SEdgeList *sel, SBezierList *sbl); + bool IsEmpty() const; + void RemapFaces(Group *g, int remap); + void Clear(); +}; + +} // namespace SolveSpace + +#endif diff --git a/src/srf/surfinter.cpp b/src/srf/surfinter.cpp new file mode 100644 index 0000000..c13c7d2 --- /dev/null +++ b/src/srf/surfinter.cpp @@ -0,0 +1,596 @@ +//----------------------------------------------------------------------------- +// How to intersect two surfaces, to get some type of curve. This is either +// an exact special case (e.g., two planes to make a line), or a numerical +// thing. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +extern int FLAG; + +void SSurface::AddExactIntersectionCurve(SBezier *sb, SSurface *srfB, + SShell *agnstA, SShell *agnstB, SShell *into) +{ + SCurve sc = {}; + // Important to keep the order of (surfA, surfB) consistent; when we later + // rewrite the identifiers, we rewrite surfA from A and surfB from B. + sc.surfA = h; + sc.surfB = srfB->h; + sc.exact = *sb; + sc.isExact = true; + + // Now we have to piecewise linearize the curve. If there's already an + // identical curve in the shell, then follow that pwl exactly, otherwise + // calculate from scratch. + SCurve split, *existing = NULL; + SBezier sbrev = *sb; + sbrev.Reverse(); + bool backwards = false; +#pragma omp critical(into) + { + for(SCurve &se : into->curve) { + if(se.isExact) { + if(sb->Equals(&(se.exact))) { + existing = &se; + break; + } + if(sbrev.Equals(&(se.exact))) { + existing = &se; + backwards = true; + break; + } + } + } + }// end omp critical + if(existing) { + SCurvePt *v; + for(v = existing->pts.First(); v; v = existing->pts.NextAfter(v)) { + sc.pts.Add(v); + } + if(backwards) sc.pts.Reverse(); + split = sc; + sc = {}; + } else { + sb->MakePwlInto(&(sc.pts)); + // and split the line where it intersects our existing surfaces + split = sc.MakeCopySplitAgainst(agnstA, agnstB, this, srfB); + sc.Clear(); + } + + // Test if the curve lies entirely outside one of the + SCurvePt *scpt; + bool withinA = false, withinB = false; + for(scpt = split.pts.First(); scpt; scpt = split.pts.NextAfter(scpt)) { + double tol = 0.01; + Point2d puv; + ClosestPointTo(scpt->p, &puv); + if(puv.x > -tol && puv.x < 1 + tol && + puv.y > -tol && puv.y < 1 + tol) + { + withinA = true; + } + srfB->ClosestPointTo(scpt->p, &puv); + if(puv.x > -tol && puv.x < 1 + tol && + puv.y > -tol && puv.y < 1 + tol) + { + withinB = true; + } + // Break out early, no sense wasting time if we already have the answer. + if(withinA && withinB) break; + } + if(!(withinA && withinB)) { + // Intersection curve lies entirely outside one of the surfaces, so + // it's fake. + split.Clear(); + return; + } + +#if 0 + if(sb->deg == 2) { + dbp(" "); + SCurvePt *prev = NULL, *v; + dbp("split.pts.n = %d", split.pts.n); + for(v = split.pts.First(); v; v = split.pts.NextAfter(v)) { + if(prev) { + Vector e = (prev->p).Minus(v->p).WithMagnitude(0); + SS.nakedEdges.AddEdge((prev->p).Plus(e), (v->p).Minus(e)); + } + prev = v; + } + } +#endif // 0 + ssassert(!(sb->Start()).Equals(sb->Finish()), + "Unexpected zero-length edge"); + + split.source = SCurve::Source::INTERSECTION; +#pragma omp critical(into) + { + into->curve.AddAndAssignId(&split); + } +} + +void SSurface::IntersectAgainst(SSurface *b, SShell *agnstA, SShell *agnstB, + SShell *into) +{ + Vector amax, amin, bmax, bmin; + GetAxisAlignedBounding(&amax, &amin); + b->GetAxisAlignedBounding(&bmax, &bmin); + + if(Vector::BoundingBoxesDisjoint(amax, amin, bmax, bmin)) { + // They cannot possibly intersect, no curves to generate + return; + } + + Vector alongt, alongb; + SBezier oft, ofb; + bool isExtdt = this->IsExtrusion(&oft, &alongt), + isExtdb = b->IsExtrusion(&ofb, &alongb); + + if(degm == 1 && degn == 1 && b->degm == 1 && b->degn == 1) { + // Line-line intersection; it's a plane or nothing. + Vector na = NormalAt(0, 0).WithMagnitude(1), + nb = b->NormalAt(0, 0).WithMagnitude(1); + double da = na.Dot(PointAt(0, 0)), + db = nb.Dot(b->PointAt(0, 0)); + + Vector dl = na.Cross(nb); + if(dl.Magnitude() < LENGTH_EPS) return; // parallel planes + dl = dl.WithMagnitude(1); + Vector p = Vector::AtIntersectionOfPlanes(na, da, nb, db); + + // Trim it to the region 0 <= {u,v} <= 1 for each plane; not strictly + // necessary, since line will be split and excess edges culled, but + // this improves speed and robustness. + int i; + double tmax = VERY_POSITIVE, tmin = VERY_NEGATIVE; + for(i = 0; i < 2; i++) { + SSurface *s = (i == 0) ? this : b; + Vector tu, tv; + s->TangentsAt(0, 0, &tu, &tv); + + double up, vp, ud, vd; + s->ClosestPointTo(p, &up, &vp); + ud = (dl.Dot(tu)) / tu.MagSquared(); + vd = (dl.Dot(tv)) / tv.MagSquared(); + + // so u = up + t*ud + // v = vp + t*vd + if(ud > LENGTH_EPS) { + tmin = max(tmin, -up/ud); + tmax = min(tmax, (1 - up)/ud); + } else if(ud < -LENGTH_EPS) { + tmax = min(tmax, -up/ud); + tmin = max(tmin, (1 - up)/ud); + } else { + if(up < -LENGTH_EPS || up > 1 + LENGTH_EPS) { + // u is constant, and outside [0, 1] + tmax = VERY_NEGATIVE; + } + } + if(vd > LENGTH_EPS) { + tmin = max(tmin, -vp/vd); + tmax = min(tmax, (1 - vp)/vd); + } else if(vd < -LENGTH_EPS) { + tmax = min(tmax, -vp/vd); + tmin = max(tmin, (1 - vp)/vd); + } else { + if(vp < -LENGTH_EPS || vp > 1 + LENGTH_EPS) { + // v is constant, and outside [0, 1] + tmax = VERY_NEGATIVE; + } + } + } + + if(tmax > tmin + LENGTH_EPS) { + SBezier bezier = SBezier::From(p.Plus(dl.ScaledBy(tmin)), + p.Plus(dl.ScaledBy(tmax))); + AddExactIntersectionCurve(&bezier, b, agnstA, agnstB, into); + } + } else if((degm == 1 && degn == 1 && isExtdb) || + (b->degm == 1 && b->degn == 1 && isExtdt)) + { + // The intersection between a plane and a surface of extrusion + SSurface *splane, *sext; + if(degm == 1 && degn == 1) { + splane = this; + sext = b; + } else { + splane = b; + sext = this; + } + + Vector n = splane->NormalAt(0, 0).WithMagnitude(1), along; + double d = n.Dot(splane->PointAt(0, 0)); + SBezier bezier; + (void)sext->IsExtrusion(&bezier, &along); + + if(fabs(n.Dot(along)) < LENGTH_EPS) { + // Direction of extrusion is parallel to plane; so intersection + // is zero or more lines. Build a line within the plane, and + // normal to the direction of extrusion, and intersect that line + // against the surface; each intersection point corresponds to + // a line. + Vector pm, alu, p0, dp; + // a point halfway along the extrusion + pm = ((sext->ctrl[0][0]).Plus(sext->ctrl[0][1])).ScaledBy(0.5); + alu = along.WithMagnitude(1); + dp = (n.Cross(along)).WithMagnitude(1); + // n, alu, and dp form an orthogonal csys; set n component to + // place it on the plane, alu component to lie halfway along + // extrusion, and dp component doesn't matter so zero + p0 = n.ScaledBy(d).Plus(alu.ScaledBy(pm.Dot(alu))); + + List inters = {}; + sext->AllPointsIntersecting(p0, p0.Plus(dp), &inters, + /*asSegment=*/false, /*trimmed=*/false, /*inclTangent=*/true); + + SInter *si; + for(si = inters.First(); si; si = inters.NextAfter(si)) { + Vector al = along.ScaledBy(0.5); + SBezier bezier; + bezier = SBezier::From((si->p).Minus(al), (si->p).Plus(al)); + AddExactIntersectionCurve(&bezier, b, agnstA, agnstB, into); + } + + inters.Clear(); + } else { + // Direction of extrusion is not parallel to plane; so + // intersection is projection of extruded curve into our plane. + int i; + for(i = 0; i <= bezier.deg; i++) { + Vector p0 = bezier.ctrl[i], + p1 = p0.Plus(along); + + bezier.ctrl[i] = + Vector::AtIntersectionOfPlaneAndLine(n, d, p0, p1, NULL); + } + + AddExactIntersectionCurve(&bezier, b, agnstA, agnstB, into); + } + } else if(isExtdt && isExtdb && + sqrt(fabs(alongt.Dot(alongb))) > + sqrt(alongt.Magnitude() * alongb.Magnitude()) - LENGTH_EPS) + { + // Two surfaces of extrusion along the same axis. So they might + // intersect along some number of lines parallel to the axis. + Vector axis = alongt.WithMagnitude(1); + + List inters = {}; + List lv = {}; + + double a_axis0 = ( ctrl[0][0]).Dot(axis), + a_axis1 = ( ctrl[0][1]).Dot(axis), + b_axis0 = (b->ctrl[0][0]).Dot(axis), + b_axis1 = (b->ctrl[0][1]).Dot(axis); + + if(a_axis0 > a_axis1) swap(a_axis0, a_axis1); + if(b_axis0 > b_axis1) swap(b_axis0, b_axis1); + + double ab_axis0 = max(a_axis0, b_axis0), + ab_axis1 = min(a_axis1, b_axis1); + + if(fabs(ab_axis0 - ab_axis1) < LENGTH_EPS) { + // The line would be zero-length + return; + } + + Vector axis0 = axis.ScaledBy(ab_axis0), + axis1 = axis.ScaledBy(ab_axis1), + axisc = (axis0.Plus(axis1)).ScaledBy(0.5); + + oft.MakePwlInto(&lv); + + int i; + for(i = 0; i < lv.n - 1; i++) { + Vector pa = lv[i], pb = lv[i+1]; + pa = pa.Minus(axis.ScaledBy(pa.Dot(axis))); + pb = pb.Minus(axis.ScaledBy(pb.Dot(axis))); + pa = pa.Plus(axisc); + pb = pb.Plus(axisc); + + b->AllPointsIntersecting(pa, pb, &inters, + /*asSegment=*/true,/*trimmed=*/false, /*inclTangent=*/false); + } + + SInter *si; + for(si = inters.First(); si; si = inters.NextAfter(si)) { + Vector p = (si->p).Minus(axis.ScaledBy((si->p).Dot(axis))); + double ub, vb; + b->ClosestPointTo(p, &ub, &vb, /*mustConverge=*/true); + SSurface plane; + plane = SSurface::FromPlane(p, axis.Normal(0), axis.Normal(1)); + + b->PointOnSurfaces(this, &plane, &ub, &vb); + + p = b->PointAt(ub, vb); + + SBezier bezier; + bezier = SBezier::From(p.Plus(axis0), p.Plus(axis1)); + AddExactIntersectionCurve(&bezier, b, agnstA, agnstB, into); + } + + inters.Clear(); + lv.Clear(); + } else { + if((degm == 1 && degn == 1) || (b->degm == 1 && b->degn == 1)) { + // we should only be here if just one surface is a plane because the + // plane-plane case was already handled above. Need to check the other + // nonplanar surface for trim curves that lie in the plane and are not + // already trimming both surfaces. This happens when we cut a Lathe shell + // on one of the seams for example. + // This also seems necessary to merge some coincident surfaces. + SSurface *splane, *sext; + SShell *shext; + if(degm == 1 && degn == 1) { // this and other checks assume coplanar ctrl pts. + splane = this; + sext = b; + shext = agnstB; + } else { + splane = b; + sext = this; + shext = agnstA; + } + bool foundExact = false; + for(SCurve &sc : shext->curve) { + if(sc.source == SCurve::Source::INTERSECTION) continue; + if(!sc.isExact) continue; + if((sc.surfA != sext->h) && (sc.surfB != sext->h)) continue; + // we have a curve belonging to the curved surface and not the plane. + // does it lie completely in the plane? + if(splane->ContainsPlaneCurve(&sc)) { + SBezier bezier = sc.exact; + AddExactIntersectionCurve(&bezier, b, agnstA, agnstB, into); + foundExact = true; + } + } + // if we found at lest one of these we don't want to do the numerical + // intersection as well. Sometimes it will also find the same curve but + // with different PWLs and the polygon will fail to assemble. + if(foundExact) + return; + } + + // Try intersecting the surfaces numerically, by a marching algorithm. + // First, we find all the intersections between a surface and the + // boundary of the other surface. + SPointList spl = {}; + int a; + for(a = 0; a < 2; a++) { + SShell *shA = (a == 0) ? agnstA : agnstB; + SSurface *srfA = (a == 0) ? this : b, + *srfB = (a == 0) ? b : this; + + SEdgeList el = {}; + srfA->MakeEdgesInto(shA, &el, MakeAs::XYZ, NULL); + + SEdge *se; + for(se = el.l.First(); se; se = el.l.NextAfter(se)) { + List lsi = {}; + + srfB->AllPointsIntersecting(se->a, se->b, &lsi, + /*asSegment=*/true, /*trimmed=*/true, /*inclTangent=*/false); + if(lsi.IsEmpty()) + continue; + + // Find the other surface that this curve trims. + hSCurve hsc = { (uint32_t)se->auxA }; + SCurve *sc = shA->curve.FindById(hsc); + hSSurface hother = (sc->surfA == srfA->h) ? + sc->surfB : sc->surfA; + SSurface *other = shA->surface.FindById(hother); + + SInter *si; + for(si = lsi.First(); si; si = lsi.NextAfter(si)) { + Vector p = si->p; + double u, v; + srfB->ClosestPointTo(p, &u, &v); + if(sc->isExact) { + srfB->PointOnCurve(&(sc->exact), &u, &v); + } else { + srfB->PointOnSurfaces(srfA, other, &u, &v); + } + p = srfB->PointAt(u, v); + if(!spl.ContainsPoint(p)) { + SPoint sp; + sp.p = p; + // We also need the edge normal, so that we know in + // which direction to march. + srfA->ClosestPointTo(p, &u, &v); + Vector n = srfA->NormalAt(u, v); + sp.auxv = n.Cross((se->b).Minus(se->a)); + sp.auxv = (sp.auxv).WithMagnitude(1); + + spl.l.Add(&sp); + } + } + lsi.Clear(); + } + + el.Clear(); + } + + while(spl.l.n >= 2) { + SCurve sc = {}; + sc.surfA = h; + sc.surfB = b->h; + sc.isExact = false; + sc.source = SCurve::Source::INTERSECTION; + + Vector start = spl.l[0].p, + startv = spl.l[0].auxv; + spl.l.ClearTags(); + spl.l[0].tag = 1; + spl.l.RemoveTagged(); + + // Our chord tolerance is whatever the user specified + double maxtol = SS.ChordTolMm(); + int maxsteps = max(300, SS.GetMaxSegments()*3); + + // The curve starts at our starting point. + SCurvePt padd = {}; + padd.vertex = true; + padd.p = start; + sc.pts.Add(&padd); + + Point2d pa, pb; + Vector np, npc = Vector::From(0, 0, 0); + bool fwd = false; + // Better to start with a too-small step, so that we don't miss + // features of the curve entirely. + double tol, step = maxtol; + for(a = 0; a < maxsteps; a++) { + ClosestPointTo(start, &pa); + b->ClosestPointTo(start, &pb); + + Vector na = NormalAt(pa).WithMagnitude(1), + nb = b->NormalAt(pb).WithMagnitude(1); + + if(a == 0) { + Vector dp = nb.Cross(na); + if(dp.Dot(startv) < 0) { + // We want to march in the more inward direction. + fwd = true; + } else { + fwd = false; + } + } + + Vector dp = nb.Cross(na).WithMagnitude(1.0); + if(!fwd) dp = dp.ScaledBy(-1.0); + int i; + for(i = 0; i < 20; i++) { + np = start.Plus(dp.ScaledBy(step)); + npc = ClosestPointOnThisAndSurface(b, np); + tol = (npc.Minus(np)).Magnitude(); + + if(tol > maxtol*0.8) { + step *= 0.90; + } else { + step /= 0.90; + } + + if((tol < maxtol) && (tol > maxtol/2)) { + // If we meet the chord tolerance test, and we're + // not too fine, then we break out. + break; + } + } + + SPoint *sp; + for(sp = spl.l.First(); sp; sp = spl.l.NextAfter(sp)) { + if((sp->p).OnLineSegment(start, npc, 2*SS.ChordTolMm())) { + sp->tag = 1; + a = maxsteps; + npc = sp->p; + } + } + + padd.p = npc; + padd.vertex = (a == maxsteps); + sc.pts.Add(&padd); + + start = npc; + } + + spl.l.RemoveTagged(); + + // And now we split and insert the curve + SCurve split = sc.MakeCopySplitAgainst(agnstA, agnstB, this, b); + sc.Clear(); +#pragma omp critical(into) + { + into->curve.AddAndAssignId(&split); + } + } + spl.Clear(); + } +} + +//----------------------------------------------------------------------------- +// Are two surfaces coincident, with the same (or with opposite) normals? +// Currently handles planes only. +//----------------------------------------------------------------------------- +bool SSurface::CoincidentWith(SSurface *ss, bool sameNormal) const { + if(degm != 1 || degn != 1) return false; + if(ss->degm != 1 || ss->degn != 1) return false; + + Vector p = ctrl[0][0]; + Vector n = NormalAt(0, 0).WithMagnitude(1); + double d = n.Dot(p); + + if(!ss->CoincidentWithPlane(n, d)) return false; + + Vector n2 = ss->NormalAt(0, 0); + if(sameNormal) { + if(n2.Dot(n) < 0) return false; + } else { + if(n2.Dot(n) > 0) return false; + } + + return true; +} + +bool SSurface::CoincidentWithPlane(Vector n, double d) const { + if(degm != 1 || degn != 1) return false; + if(fabs(n.Dot(ctrl[0][0]) - d) > LENGTH_EPS) return false; + if(fabs(n.Dot(ctrl[0][1]) - d) > LENGTH_EPS) return false; + if(fabs(n.Dot(ctrl[1][0]) - d) > LENGTH_EPS) return false; + if(fabs(n.Dot(ctrl[1][1]) - d) > LENGTH_EPS) return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Does a planar surface contain a curve? Does the curve lie completely in plane? +//----------------------------------------------------------------------------- +bool SSurface::ContainsPlaneCurve(SCurve *sc) const { + if(degm != 1 || degn != 1) return false; + if(!sc->isExact) return false; // we don't handle those (yet?) + + Vector p = ctrl[0][0]; + Vector n = NormalAt(0, 0).WithMagnitude(1); + double d = n.Dot(p); + + // check all control points on the curve + for(int i=0; i<= sc->exact.deg; i++) { + if(fabs(n.Dot(sc->exact.ctrl[i]) - d) > LENGTH_EPS) return false; + } + return true; +} + +//----------------------------------------------------------------------------- +// In our shell, find all surfaces that are coincident with the prototype +// surface (with same or opposite normal, as specified), and copy all of +// their trim polygons into el. The edges are returned in uv coordinates for +// the prototype surface. +//----------------------------------------------------------------------------- +void SShell::MakeCoincidentEdgesInto(SSurface *proto, bool sameNormal, + SEdgeList *el, SShell *useCurvesFrom) +{ + for(SSurface &ss : surface) { + if(proto->CoincidentWith(&ss, sameNormal)) { + ss.MakeEdgesInto(this, el, SSurface::MakeAs::XYZ, useCurvesFrom); + } + } + + SEdge *se; + for(se = el->l.First(); se; se = el->l.NextAfter(se)) { + double ua, va, ub, vb; + proto->ClosestPointTo(se->a, &ua, &va); + proto->ClosestPointTo(se->b, &ub, &vb); + + if(sameNormal) { + se->a = Vector::From(ua, va, 0); + se->b = Vector::From(ub, vb, 0); + } else { + // Flip normal, so flip all edge directions + se->b = Vector::From(ua, va, 0); + se->a = Vector::From(ub, vb, 0); + } + } +} + +} // namespace SolveSpace diff --git a/src/srf/triangulate.cpp b/src/srf/triangulate.cpp new file mode 100644 index 0000000..81a2a45 --- /dev/null +++ b/src/srf/triangulate.cpp @@ -0,0 +1,738 @@ +//----------------------------------------------------------------------------- +// Triangulate a surface. If the surface is curved, then we first superimpose +// a grid of quads, with spacing to achieve our chord tolerance. We then +// proceed by ear-clipping; the resulting mesh should be watertight and not +// awful numerically, but has no special properties (Delaunay, etc.). +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +void SPolygon::UvTriangulateInto(SMesh *m, SSurface *srf) { + if(l.n <= 0) return; + + //int64_t in = GetMilliseconds(); + + normal = Vector::From(0, 0, 1); + + while(l.n > 0) { + FixContourDirections(); + l.ClearTags(); + + // Find a top-level contour, and start with that. Then build bridges + // in order to merge all its islands into a single contour. + SContour *top; + for(top = l.First(); top; top = l.NextAfter(top)) { + if(top->timesEnclosed == 0) { + break; + } + } + if(!top) { + dbp("polygon has no top-level contours?"); + return; + } + + // Start with the outer contour + SContour merged = {}; + top->tag = 1; + top->CopyInto(&merged); + merged.l.RemoveLast(1); + + // List all of the edges, for testing whether bridges work. + SEdgeList el = {}; + top->MakeEdgesInto(&el); + List vl = {}; + + // And now find all of its holes. Note that we will also find any + // outer contours that lie entirely within this contour, and any + // holes for those contours. But that's okay, because we can merge + // those too. + SContour *sc; + for(sc = l.First(); sc; sc = l.NextAfter(sc)) { + if(sc->timesEnclosed != 1) continue; + if(sc->l.n < 2) continue; + + // Test the midpoint of an edge. Our polygon may not be self- + // intersecting, but two contours may share a vertex; so a + // vertex could be on the edge of another polygon, in which + // case ContainsPointProjdToNormal returns indeterminate. + Vector tp = sc->AnyEdgeMidpoint(); + if(top->ContainsPointProjdToNormal(normal, tp)) { + sc->tag = 2; + sc->MakeEdgesInto(&el); + sc->FindPointWithMinX(); + } + } + +// dbp("finished finding holes: %d ms", (int)(GetMilliseconds() - in)); + for(;;) { + double xmin = 1e10; + SContour *scmin = NULL; + + for(sc = l.First(); sc; sc = l.NextAfter(sc)) { + if(sc->tag != 2) continue; + + if(sc->xminPt.x < xmin) { + xmin = sc->xminPt.x; + scmin = sc; + } + } + if(!scmin) break; + + if(!merged.BridgeToContour(scmin, &el, &vl)) { + dbp("couldn't merge our hole"); + return; + } +// dbp(" bridged to contour: %d ms", (int)(GetMilliseconds() - in)); + scmin->tag = 3; + } +// dbp("finished merging holes: %d ms", (int)(GetMilliseconds() - in)); + + merged.UvTriangulateInto(m, srf); +// dbp("finished ear clippping: %d ms", (int)(GetMilliseconds() - in)); + merged.l.Clear(); + el.Clear(); + vl.Clear(); + + // Careful, need to free the points within the contours, and not just + // the contours themselves. This was a tricky memory leak. + for(sc = l.First(); sc; sc = l.NextAfter(sc)) { + if(sc->tag) { + sc->l.Clear(); + } + } + l.RemoveTagged(); + } +} + +bool SContour::BridgeToContour(SContour *sc, + SEdgeList *avoidEdges, List *avoidPts) +{ + int i, j; + bool withbridge = true; + + // Start looking for a bridge on our new hole near its leftmost (min x) + // point. + int sco = 0; + for(i = 0; i < (sc->l.n - 1); i++) { + if((sc->l[i].p).EqualsExactly(sc->xminPt)) { + sco = i; + } + } + + // And start looking on our merged contour at whichever point is nearest + // to the leftmost point of the new segment. + int thiso = 0; + double dmin = 1e10; + for(i = 0; i < l.n-1; i++) { + Vector p = l[i].p; + double d = (p.Minus(sc->xminPt)).MagSquared(); + if(d < dmin) { + dmin = d; + thiso = i; + } + } + + int thisp, scp; + + Vector a, b, *f; + + // First check if the contours share a point; in that case we should + // merge them there, without a bridge. + for(i = 0; i < l.n; i++) { + thisp = WRAP(i+thiso, l.n-1); + a = l[thisp].p; + + for(f = avoidPts->First(); f; f = avoidPts->NextAfter(f)) { + if(f->Equals(a)) break; + } + if(f) continue; + + for(j = 0; j < (sc->l.n - 1); j++) { + scp = WRAP(j+sco, (sc->l.n - 1)); + b = sc->l[scp].p; + + if(a.Equals(b)) { + withbridge = false; + goto haveEdge; + } + } + } + + // If that fails, look for a bridge that does not intersect any edges. + for(i = 0; i < l.n; i++) { + thisp = WRAP(i+thiso, l.n); + a = l[thisp].p; + + for(f = avoidPts->First(); f; f = avoidPts->NextAfter(f)) { + if(f->Equals(a)) break; + } + if(f) continue; + + for(j = 0; j < (sc->l.n - 1); j++) { + scp = WRAP(j+sco, (sc->l.n - 1)); + b = sc->l[scp].p; + + for(f = avoidPts->First(); f; f = avoidPts->NextAfter(f)) { + if(f->Equals(b)) break; + } + if(f) continue; + + if(avoidEdges->AnyEdgeCrossings(a, b) > 0) { + // doesn't work, bridge crosses an existing edge + } else { + goto haveEdge; + } + } + } + + // Tried all the possibilities, didn't find an edge + return false; + +haveEdge: + SContour merged = {}; + for(i = 0; i < l.n; i++) { + if(withbridge || (i != thisp)) { + merged.AddPoint(l[i].p); + } + if(i == thisp) { + // less than or equal; need to duplicate the join point + for(j = 0; j <= (sc->l.n - 1); j++) { + int jp = WRAP(j + scp, (sc->l.n - 1)); + merged.AddPoint((sc->l[jp]).p); + } + // and likewise duplicate join point for the outer curve + if(withbridge) { + merged.AddPoint(l[i].p); + } + } + } + + // and future bridges mustn't cross our bridge, and it's tricky to get + // things right if two bridges come from the same point + if(withbridge) { + avoidEdges->AddEdge(a, b); + avoidPts->Add(&a); + } + avoidPts->Add(&b); + + l.Clear(); + l = merged.l; + return true; +} + +bool SContour::IsEmptyTriangle(int ap, int bp, int cp, double scaledEPS) const { + + STriangle tr = {}; + tr.a = l[ap].p; + tr.b = l[bp].p; + tr.c = l[cp].p; + + // Accelerate with an axis-aligned bounding box test + Vector maxv = tr.a, minv = tr.a; + (tr.b).MakeMaxMin(&maxv, &minv); + (tr.c).MakeMaxMin(&maxv, &minv); + + Vector n = Vector::From(0, 0, -1); + + int i; + for(i = 0; i < l.n; i++) { + if(i == ap || i == bp || i == cp) continue; + + Vector p = l[i].p; + if(p.OutsideAndNotOn(maxv, minv)) continue; + + // A point on the edge of the triangle is considered to be inside, + // and therefore makes it a non-ear; but a point on the vertex is + // "outside", since that's necessary to make bridges work. + if(p.EqualsExactly(tr.a)) continue; + if(p.EqualsExactly(tr.b)) continue; + if(p.EqualsExactly(tr.c)) continue; + + if(tr.ContainsPointProjd(n, p)) { + return false; + } + } + return true; +} + +// Test if ray b->d passes through triangle a,b,c +static bool RayIsInside(Vector a, Vector c, Vector b, Vector d) { + // coincident edges are not considered to intersect the triangle + if (d.Equals(a)) return false; + if (d.Equals(c)) return false; + // if d and c are on opposite sides of ba, we are ok + // likewise if d and a are on opposite sides of bc + Vector ba = a.Minus(b); + Vector bc = c.Minus(b); + Vector bd = d.Minus(b); + + // perpendicular to (x,y) is (x,-y) so dot that with the two points. If they + // have opposite signs their product will be negative. If bd and bc are on + // opposite sides of ba the ray does not intersect. Likewise for bd,ba and bc. + if ( (bd.x*(ba.y) + (bd.y * (-ba.x))) * ( bc.x*(ba.y) + (bc.y * (-ba.x))) < LENGTH_EPS) + return false; + if ( (bd.x*(bc.y) + (bd.y * (-bc.x))) * ( ba.x*(bc.y) + (ba.y * (-bc.x))) < LENGTH_EPS) + return false; + + return true; +} + +bool SContour::IsEar(int bp, double scaledEps) const { + int ap = WRAP(bp-1, l.n), + cp = WRAP(bp+1, l.n); + + STriangle tr = {}; + tr.a = l[ap].p; + tr.b = l[bp].p; + tr.c = l[cp].p; + + if((tr.a).Equals(tr.c)) { + // This is two coincident and anti-parallel edges. Zero-area, so + // won't generate a real triangle, but we certainly can clip it. + return true; + } + + Vector n = Vector::From(0, 0, -1); + if((tr.Normal()).Dot(n) < scaledEps) { + // This vertex is reflex, or between two collinear edges; either way, + // it's not an ear. + return false; + } + + // Accelerate with an axis-aligned bounding box test + Vector maxv = tr.a, minv = tr.a; + (tr.b).MakeMaxMin(&maxv, &minv); + (tr.c).MakeMaxMin(&maxv, &minv); + + int i; + for(i = 0; i < l.n; i++) { + if(i == ap || i == bp || i == cp) continue; + + Vector p = l[i].p; + if(p.OutsideAndNotOn(maxv, minv)) continue; + + // A point on the edge of the triangle is considered to be inside, + // and therefore makes it a non-ear; but a point on the vertex is + // "outside", since that's necessary to make bridges work. + if(p.EqualsExactly(tr.a)) continue; + if(p.EqualsExactly(tr.c)) continue; + // points coincident with bp have to be allowed for bridges but edges + // from that other point must not cross through our triangle. + if(p.EqualsExactly(tr.b)) { + int j = WRAP(i-1, l.n); + int k = WRAP(i+1, l.n); + Vector jp = l[j].p; + Vector kp = l[k].p; + + // two consecutive bridges (A,B,C) and later (C,B,A) are not an ear + if (jp.Equals(tr.c) && kp.Equals(tr.a)) return false; + // check both edges from the point in question + if (!RayIsInside(tr.a, tr.c, p,jp) && !RayIsInside(tr.a, tr.c, p,kp)) + continue; + } + + if(tr.ContainsPointProjd(n, p)) { + return false; + } + } + return true; +} + +void SContour::ClipEarInto(SMesh *m, int bp, double scaledEps) { + int ap = WRAP(bp-1, l.n), + cp = WRAP(bp+1, l.n); + + STriangle tr = {}; + tr.a = l[ap].p; + tr.b = l[bp].p; + tr.c = l[cp].p; + if(tr.Normal().MagSquared() < scaledEps*scaledEps) { + // A vertex with more than two edges will cause us to generate + // zero-area triangles, which must be culled. + } else { + m->AddTriangle(&tr); + } + + // By deleting the point at bp, we may change the ear-ness of the points + // on either side. + l[ap].ear = EarType::UNKNOWN; + l[cp].ear = EarType::UNKNOWN; + + l.ClearTags(); + l[bp].tag = 1; + l.RemoveTagged(); +} + +void SContour::UvTriangulateInto(SMesh *m, SSurface *srf) { + Vector tu, tv; + srf->TangentsAt(0.5, 0.5, &tu, &tv); + double s = sqrt(tu.MagSquared() + tv.MagSquared()); + // We would like to apply our tolerances in xyz; but that would be a lot + // of work, so at least scale the epsilon semi-reasonably. That's + // perfect for square planes, less perfect for anything else. + double scaledEps = LENGTH_EPS / s; + + int i; + // Clean the original contour by removing any zero-length edges. + // initialize eartypes to unknown while we're going over them. + l.ClearTags(); + l[0].ear = EarType::UNKNOWN; + for(i = 1; i < l.n; i++) { + l[i].ear = EarType::UNKNOWN; + if((l[i].p).Equals(l[i-1].p)) { + l[i].tag = 1; + } + } + if( (l[0].p).Equals(l[l.n-1].p) ) { + l[l.n-1].tag = 1; + } + l.RemoveTagged(); + + // Handle simple triangle fans all at once. This pass is optional. + if(srf->degm == 1 && srf->degn == 1) { + l.ClearTags(); + int j=0; + int pstart = 0; + double elen = -1.0; + double oldspan = 0.0; + for(i = 1; i < l.n; i++) { + Vector ab = l[i].p.Minus(l[i-1].p); + // first time just measure the segment + if (elen < 0.0) { + elen = ab.Dot(ab); + oldspan = elen; + j = 1; + continue; + } + // check for consecutive segments of similar size which are also + // ears and where the group forms a convex ear + bool end = false; + double ratio = ab.Dot(ab) / elen; + if ((ratio < 0.25) || (ratio > 4.0)) end = true; + + double slen = l[pstart].p.Minus(l[i].p).MagSquared(); + if (slen < oldspan) end = true; + + if (!IsEar(i-1, scaledEps) ) end = true; +// if ((j>0) && !IsEar(pstart, i-1, i, scaledEps)) end = true; + if ((j>0) && !IsEmptyTriangle(pstart, i-1, i, scaledEps)) end = true; + // the new segment is valid so add to the fan + if (!end) { + j++; + oldspan = slen; + } + // we need to stop at the end of polygon but may still + if (i == l.n-1) { + end = true; + } + if (end) { // triangulate the fan and tag the vertices + if (j > 3) { + Vector center = l[pstart+1].p.Plus(l[pstart+j-1].p).ScaledBy(0.5); + for (int x=0; xAddTriangle(&tr); + } + for (int x=1; xAddTriangle(&tr); + } + pstart = i-1; + elen = ab.Dot(ab); + oldspan = elen; + j = 1; + } + } + l.RemoveTagged(); + } // end optional fan creation pass + + bool toggle = false; + while(l.n > 3) { + int bestEar = -1; + double bestChordTol = VERY_POSITIVE; + // Alternate the starting position so we generate strip-like + // triangulations instead of fan-like + toggle = !toggle; + int offset = toggle ? -1 : 0; + for(i = 0; i < l.n; i++) { + int ear = WRAP(i+offset, l.n); + if(l[ear].ear == EarType::UNKNOWN) { + (l[ear]).ear = IsEar(ear, scaledEps) ? EarType::EAR : EarType::NOT_EAR; + } + if(l[ear].ear == EarType::EAR) { + if(srf->degm == 1 && srf->degn == 1) { + // This is a plane; any ear is a good ear. + bestEar = ear; + break; + } + // If we are triangulating a curved surface, then try to + // clip ears that have a small chord tolerance from the + // surface. + Vector prev = l[WRAP((i+offset-1), l.n)].p, + next = l[WRAP((i+offset+1), l.n)].p; + double tol = srf->ChordToleranceForEdge(prev, next); + if(tol < bestChordTol - scaledEps) { + bestEar = ear; + bestChordTol = tol; + } + if(bestChordTol < 0.1*SS.ChordTolMm()) { + break; + } + } + } + if(bestEar < 0) { + dbp("couldn't find an ear! fail"); + return; + } + ClipEarInto(m, bestEar, scaledEps); + } + + ClipEarInto(m, 0, scaledEps); // add the last triangle +} + +double SSurface::ChordToleranceForEdge(Vector a, Vector b) const { + Vector as = PointAt(a.x, a.y), bs = PointAt(b.x, b.y); + + double worst = VERY_NEGATIVE; + int i; + for(i = 1; i <= 3; i++) { + Vector p = a. Plus((b. Minus(a )).ScaledBy(i/4.0)), + ps = as.Plus((bs.Minus(as)).ScaledBy(i/4.0)); + + Vector pps = PointAt(p.x, p.y); + worst = max(worst, (pps.Minus(ps)).MagSquared()); + } + return sqrt(worst); +} + +Vector SSurface::PointAtMaybeSwapped(double u, double v, bool swapped) const { + if(swapped) { + return PointAt(v, u); + } else { + return PointAt(u, v); + } +} + +Vector SSurface::NormalAtMaybeSwapped(double u, double v, bool swapped) const { + Vector du, dv; + if(swapped) { + TangentsAt(v, u, &dv, &du); + } else { + TangentsAt(u, v, &du, &dv); + } + return du.Cross(dv).WithMagnitude(1.0); +} + +void SSurface::MakeTriangulationGridInto(List *l, double vs, double vf, + bool swapped, int depth) const +{ + double worst = 0; + + // Try piecewise linearizing four curves, at u = 0, 1/3, 2/3, 1; choose + // the worst chord tolerance of any of those. + double worst_twist = 1.0; + int i; + for(i = 0; i <= 3; i++) { + double u = i/3.0; + + // This chord test should be identical to the one in SBezier::MakePwl + // to make the piecewise linear edges line up with the grid more or + // less. + Vector ps = PointAtMaybeSwapped(u, vs, swapped), + pf = PointAtMaybeSwapped(u, vf, swapped); + + double vm1 = (2*vs + vf) / 3, + vm2 = (vs + 2*vf) / 3; + + Vector pm1 = PointAtMaybeSwapped(u, vm1, swapped), + pm2 = PointAtMaybeSwapped(u, vm2, swapped); + + // 0.999 is about 2.5 degrees of twist over the middle 1/3 V-span. + // we don't check at the ends because the derivative may not be valid there. + double twist = 1.0; + if (degm == 1) twist = NormalAtMaybeSwapped(u, vm1, swapped).Dot( + NormalAtMaybeSwapped(u, vm2, swapped) ); + if (twist < worst_twist) worst_twist = twist; + + worst = max(worst, pm1.DistanceToLine(ps, pf.Minus(ps))); + worst = max(worst, pm2.DistanceToLine(ps, pf.Minus(ps))); + } + + double step = 1.0/SS.GetMaxSegments(); + if( ((vf - vs) < step || worst < SS.ChordTolMm()) + && ((worst_twist > 0.999) || (depth > 3)) ) { + l->Add(&vf); + } else { + MakeTriangulationGridInto(l, vs, (vs+vf)/2, swapped, depth+1); + MakeTriangulationGridInto(l, (vs+vf)/2, vf, swapped, depth+1); + } +} + +void SPolygon::UvGridTriangulateInto(SMesh *mesh, SSurface *srf) { + SEdgeList orig = {}; + MakeEdgesInto(&orig); + + SEdgeList holes = {}; + + normal = Vector::From(0, 0, 1); + FixContourDirections(); + + // Build a rectangular grid, with horizontal and vertical lines in the + // uv plane. The spacing of these lines is adaptive, so calculate that. + List li, lj; + li = {}; + lj = {}; + double v[5] = {0.0, 0.25, 0.5, 0.75, 1.0}; + li.Add(&v[0]); + srf->MakeTriangulationGridInto(&li, 0, 1, /*swapped=*/true, 0); + lj.Add(&v[0]); + srf->MakeTriangulationGridInto(&lj, 0, 1, /*swapped=*/false, 0); + + // force 2nd order grid to have at least 4 segments in each direction + if ((li.n < 5) && (srf->degm>1)) { // 4 segments minimum + li.Clear(); + li.Add(&v[0]);li.Add(&v[1]);li.Add(&v[2]);li.Add(&v[3]);li.Add(&v[4]); + } + if ((lj.n < 5) && (srf->degn>1)) { // 4 segments minimum + lj.Clear(); + lj.Add(&v[0]);lj.Add(&v[1]);lj.Add(&v[2]);lj.Add(&v[3]);lj.Add(&v[4]); + } + + if ((li.n > 3) && (lj.n > 3)) { + // Now iterate over each quad in the grid. If it's outside the polygon, + // or if it intersects the polygon, then we discard it. Otherwise we + // generate two triangles in the mesh, and cut it out of our polygon. + // Quads around the perimeter would be rejected by AnyEdgeCrossings. + std::vector bottom(lj.n, false); // did we use this quad? + Vector tu = {0,0,0}, tv = {0,0,0}; + int i, j; + for(i = 1; i < (li.n-1); i++) { + bool prev_flag = false; + for(j = 1; j < (lj.n-1); j++) { + bool this_flag = true; + double us = li[i], uf = li[i+1], + vs = lj[j], vf = lj[j+1]; + + Vector a = Vector::From(us, vs, 0), + b = Vector::From(us, vf, 0), + c = Vector::From(uf, vf, 0), + d = Vector::From(uf, vs, 0); + + // | d-----c + // | | | + // | | | + // | a-----b + // | + // +-------------> j/v axis + + if( (i==(li.n-2)) || (j==(lj.n-2)) || + orig.AnyEdgeCrossings(a, b, NULL) || + orig.AnyEdgeCrossings(b, c, NULL) || + orig.AnyEdgeCrossings(c, d, NULL) || + orig.AnyEdgeCrossings(d, a, NULL)) + { + this_flag = false; + } + + // There's no intersections, so it doesn't matter which point + // we decide to test. + if(!this->ContainsPoint(a)) { + this_flag = false; + } + + if (this_flag) { + // Add the quad to our mesh + srf->TangentsAt(us,vs, &tu,&tv); + if(tu.Dot(tv) < LENGTH_EPS) { + /* Split "the other way" if angle>90 + compare to LENGTH_EPS instead of zero to avoid alternating triangle + "orientations" when the tangents are orthogonal (revolve, lathe etc.) + this results in a higher quality mesh. */ + STriangle tr = {}; + tr.a = a; + tr.b = b; + tr.c = c; + mesh->AddTriangle(&tr); + tr.a = a; + tr.b = c; + tr.c = d; + mesh->AddTriangle(&tr); + } else{ + STriangle tr = {}; + tr.a = a; + tr.b = b; + tr.c = d; + mesh->AddTriangle(&tr); + tr.a = b; + tr.b = c; + tr.c = d; + mesh->AddTriangle(&tr); + } + if (!prev_flag) // add our own left edge + holes.AddEdge(d, a); + if (!bottom[j]) // add our own bottom edge + holes.AddEdge(a, b); + } else { + if (prev_flag) // add our left neighbors right edge + holes.AddEdge(a, d); + if (bottom[j]) // add our bottom neighbors top edge + holes.AddEdge(b, a); + } + prev_flag = this_flag; + bottom[j] = this_flag; + } + } + + // Because no duplicate edges were created we do not need to cull them. + SPolygon hp = {}; + holes.AssemblePolygon(&hp, NULL, /*keepDir=*/true); + + SContour *sc; + for(sc = hp.l.First(); sc; sc = hp.l.NextAfter(sc)) { + l.Add(sc); + } + hp.l.Clear(); + } + orig.Clear(); + holes.Clear(); + li.Clear(); + lj.Clear(); + UvTriangulateInto(mesh, srf); +} + +void SPolygon::TriangulateInto(SMesh *m) const { + Vector n = normal; + if(n.Equals(Vector::From(0.0, 0.0, 0.0))) { + n = ComputeNormal(); + } + Vector u = n.Normal(0); + Vector v = n.Normal(1); + + SPolygon p = {}; + this->InverseTransformInto(&p, u, v, n); + + SSurface srf = SSurface::FromPlane(Vector::From(0.0, 0.0, 0.0), + Vector::From(1.0, 0.0, 0.0), + Vector::From(0.0, 1.0, 0.0)); + SMesh pm = {}; + p.UvTriangulateInto(&pm, &srf); + for(STriangle st : pm.l) { + st = st.Transform(u, v, n); + m->AddTriangle(&st); + } + + p.Clear(); + pm.Clear(); +} + +} // namespace SolveSpace diff --git a/src/style.cpp b/src/style.cpp new file mode 100644 index 0000000..950b04e --- /dev/null +++ b/src/style.cpp @@ -0,0 +1,948 @@ +//----------------------------------------------------------------------------- +// Implementation of a cosmetic line style, which determines the color and +// other appearance of a line or curve on-screen and in exported files. Some +// styles are predefined, and others can be created by the user. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +const Style::Default Style::Defaults[] = { + { { ACTIVE_GRP }, "ActiveGrp", RGBf(1.0, 1.0, 1.0), 1.5, 4, true, StipplePattern::CONTINUOUS }, + { { CONSTRUCTION }, "Construction", RGBf(0.1, 0.7, 0.1), 1.5, 0, false, StipplePattern::CONTINUOUS }, + { { INACTIVE_GRP }, "InactiveGrp", RGBf(0.5, 0.3, 0.0), 1.5, 3, true, StipplePattern::CONTINUOUS }, + { { DATUM }, "Datum", RGBf(0.0, 0.8, 0.0), 1.5, 0, true, StipplePattern::CONTINUOUS }, + { { SOLID_EDGE }, "SolidEdge", RGBf(0.8, 0.8, 0.8), 1.0, 2, true, StipplePattern::CONTINUOUS }, + { { CONSTRAINT }, "Constraint", RGBf(1.0, 0.1, 1.0), 1.0, 0, true, StipplePattern::CONTINUOUS }, + { { SELECTED }, "Selected", RGBf(1.0, 0.0, 0.0), 1.5, 0, true, StipplePattern::CONTINUOUS }, + { { HOVERED }, "Hovered", RGBf(1.0, 1.0, 0.0), 1.5, 0, true, StipplePattern::CONTINUOUS }, + { { CONTOUR_FILL }, "ContourFill", RGBf(0.0, 0.1, 0.1), 1.0, 0, true, StipplePattern::CONTINUOUS }, + { { NORMALS }, "Normals", RGBf(0.0, 0.4, 0.4), 1.0, 0, true, StipplePattern::CONTINUOUS }, + { { ANALYZE }, "Analyze", RGBf(0.0, 1.0, 1.0), 3.0, 0, true, StipplePattern::CONTINUOUS }, + { { DRAW_ERROR }, "DrawError", RGBf(1.0, 0.0, 0.0), 8.0, 0, true, StipplePattern::CONTINUOUS }, + { { DIM_SOLID }, "DimSolid", RGBf(0.1, 0.1, 0.1), 1.0, 0, true, StipplePattern::CONTINUOUS }, + { { HIDDEN_EDGE }, "HiddenEdge", RGBf(0.8, 0.8, 0.8), 1.0, 1, true, StipplePattern::DASH }, + { { OUTLINE }, "Outline", RGBf(0.8, 0.8, 0.8), 3.0, 5, true, StipplePattern::CONTINUOUS }, + { { 0 }, NULL, RGBf(0.0, 0.0, 0.0), 0.0, 0, true, StipplePattern::CONTINUOUS } +}; + +std::string Style::CnfColor(const std::string &prefix) { + return "Style_" + prefix + "_Color"; +} +std::string Style::CnfWidth(const std::string &prefix) { + return "Style_" + prefix + "_Width"; +} +std::string Style::CnfStippleType(const std::string &prefix) { + return "Style_" + prefix + "_StippleType"; +} +std::string Style::CnfStippleScale(const std::string &prefix) { + return "Style_" + prefix + "_StippleScale"; +} +std::string Style::CnfTextHeight(const std::string &prefix) { + return "Style_" + prefix + "_TextHeight"; +} +std::string Style::CnfExportable(const std::string &prefix) { + return "Style_" + prefix + "_Exportable"; +} + +std::string Style::CnfPrefixToName(const std::string &prefix) { + std::string name = "#def-"; + + for(size_t i = 0; i < prefix.length(); i++) { + if(isupper(prefix[i]) && i != 0) + name += '-'; + name += tolower(prefix[i]); + } + + return name; +} + +void Style::CreateAllDefaultStyles() { + const Default *d; + for(d = &(Defaults[0]); d->h.v; d++) { + (void)Get(d->h); + } +} + +void Style::CreateDefaultStyle(hStyle h) { + bool isDefaultStyle = true; + const Default *d; + for(d = &(Defaults[0]); d->h.v; d++) { + if(d->h == h) break; + } + if(!d->h.v) { + // Not a default style; so just create it the same as our default + // active group entity style. + d = &(Defaults[0]); + isDefaultStyle = false; + } + + Style ns = {}; + FillDefaultStyle(&ns, d); + ns.h = h; + if(isDefaultStyle) { + ns.name = CnfPrefixToName(d->cnfPrefix); + } else { + ns.name = "new-custom-style"; + } + + SK.style.Add(&ns); +} + +void Style::FillDefaultStyle(Style *s, const Default *d, bool factory) { + Platform::SettingsRef settings = Platform::GetSettings(); + + if(d == NULL) d = &Defaults[0]; + s->color = (factory) + ? d->color + : settings->ThawColor(CnfColor(d->cnfPrefix), d->color); + s->width = (factory) + ? d->width + : settings->ThawFloat(CnfWidth(d->cnfPrefix), (float)(d->width)); + s->widthAs = UnitsAs::PIXELS; + s->textHeight = (factory) ? 11.5 + : settings->ThawFloat(CnfTextHeight(d->cnfPrefix), 11.5); + s->textHeightAs = UnitsAs::PIXELS; + s->textOrigin = TextOrigin::NONE; + s->textAngle = 0; + s->visible = true; + s->exportable = (factory) + ? d->exportable + : settings->ThawBool(CnfExportable(d->cnfPrefix), d->exportable); + s->filled = false; + s->fillColor = RGBf(0.3, 0.3, 0.3); + s->stippleType = (factory) + ? d->stippleType + : Style::StipplePatternFromString( + settings->ThawString(CnfStippleType(d->cnfPrefix), + StipplePatternName(d->stippleType))); + s->stippleScale = (factory) + ? 15.0 + : settings->ThawFloat(CnfStippleScale(d->cnfPrefix), 15.0); + s->zIndex = d->zIndex; +} + +void Style::LoadFactoryDefaults() { + const Default *d; + for(d = &(Defaults[0]); d->h.v; d++) { + Style *s = Get(d->h); + FillDefaultStyle(s, d, /*factory=*/true); + } + SS.backgroundColor = RGBi(0, 0, 0); +} + +void Style::FreezeDefaultStyles(Platform::SettingsRef settings) { + const Default *d; + for(d = &(Defaults[0]); d->h.v; d++) { + settings->FreezeColor(CnfColor(d->cnfPrefix), Color(d->h)); + settings->FreezeFloat(CnfWidth(d->cnfPrefix), (float)Width(d->h)); + settings->FreezeString(CnfStippleType(d->cnfPrefix), StipplePatternName(d->h)); + settings->FreezeFloat(CnfStippleScale(d->cnfPrefix), (float)StippleScale(d->h)); + settings->FreezeFloat(CnfTextHeight(d->cnfPrefix), (float)TextHeight(d->h)); + settings->FreezeBool(CnfExportable(d->cnfPrefix), Exportable(d->h.v)); + } +} + +uint32_t Style::CreateCustomStyle(bool rememberForUndo) { + if(rememberForUndo) SS.UndoRemember(); + uint32_t vs = max((uint32_t)Style::FIRST_CUSTOM, SK.style.MaximumId() + 1); + hStyle hs = { vs }; + (void)Style::Get(hs); + return hs.v; +} + +void Style::AssignSelectionToStyle(uint32_t v) { + bool showError = false; + SS.GW.GroupSelection(); + + SS.UndoRemember(); + int i; + for(i = 0; i < SS.GW.gs.entities; i++) { + hEntity he = SS.GW.gs.entity[i]; + Entity *e = SK.GetEntity(he); + if(!e->IsStylable()) continue; + + if(!he.isFromRequest()) { + showError = true; + continue; + } + + hRequest hr = he.request(); + Request *r = SK.GetRequest(hr); + r->style.v = v; + SS.MarkGroupDirty(r->group); + } + for(i = 0; i < SS.GW.gs.constraints; i++) { + hConstraint hc = SS.GW.gs.constraint[i]; + Constraint *c = SK.GetConstraint(hc); + if(!c->IsStylable()) continue; + + c->disp.style.v = v; + SS.MarkGroupDirty(c->group); + } + + if(showError) { + Error(_("Can't assign style to an entity that's derived from another " + "entity; try assigning a style to this entity's parent.")); + } + + SS.GW.ClearSelection(); + SS.GW.Invalidate(); + + // And show that style's info screen in the text window. + SS.TW.GoToScreen(TextWindow::Screen::STYLE_INFO); + SS.TW.shown.style.v = v; + SS.ScheduleShowTW(); +} + +//----------------------------------------------------------------------------- +// Look up a style by its handle. If that style does not exist, then create +// the style, according to our table of default styles. +//----------------------------------------------------------------------------- +Style *Style::Get(hStyle h) { + if(h.v == 0) h.v = ACTIVE_GRP; + + Style *s = SK.style.FindByIdNoOops(h); + if(s) { + // It exists, good. + return s; + } else { + // It doesn't exist; so we should create it and then return that. + CreateDefaultStyle(h); + return SK.style.FindById(h); + } +} + +//----------------------------------------------------------------------------- +// A couple of wrappers, so that I can call these functions with either an +// hStyle or with the integer corresponding to that hStyle.v. +//----------------------------------------------------------------------------- +RgbaColor Style::Color(int s, bool forExport) { + hStyle hs = { (uint32_t)s }; + return Color(hs, forExport); +} +double Style::Width(int s) { + hStyle hs = { (uint32_t)s }; + return Width(hs); +} + +//----------------------------------------------------------------------------- +// If a color is almost white, then we can rewrite it to black, just so that +// it won't disappear on file formats with a light background. +//----------------------------------------------------------------------------- +RgbaColor Style::RewriteColor(RgbaColor rgbin) { + Vector rgb = Vector::From(rgbin.redF(), rgbin.greenF(), rgbin.blueF()); + rgb = rgb.Minus(Vector::From(1, 1, 1)); + if(rgb.Magnitude() < 0.4 && SS.fixExportColors) { + // This is an almost-white color in a default style, which is + // good for the default on-screen view (black bg) but probably + // not desired in the exported files, which typically are shown + // against white backgrounds. + return RGBi(0, 0, 0); + } else { + return rgbin; + } +} + +//----------------------------------------------------------------------------- +// Return the stroke color associated with our style as 8-bit RGB. +//----------------------------------------------------------------------------- +RgbaColor Style::Color(hStyle h, bool forExport) { + Style *s = Get(h); + if(forExport) { + return RewriteColor(s->color); + } else { + return s->color; + } +} + +//----------------------------------------------------------------------------- +// Return the fill color associated with our style as 8-bit RGB. +//----------------------------------------------------------------------------- +RgbaColor Style::FillColor(hStyle h, bool forExport) { + Style *s = Get(h); + if(forExport) { + return RewriteColor(s->fillColor); + } else { + return s->fillColor; + } +} + +//----------------------------------------------------------------------------- +// Return the width associated with our style in pixels.. +//----------------------------------------------------------------------------- +double Style::Width(hStyle h) { + Style *s = Get(h); + switch(s->widthAs) { + case UnitsAs::MM: return s->width * SS.GW.scale; + case UnitsAs::PIXELS: return s->width; + } + ssassert(false, "Unexpected units"); +} + +//----------------------------------------------------------------------------- +// Return the width associated with our style in millimeters.. +//----------------------------------------------------------------------------- +double Style::WidthMm(int hs) { + double widthpx = Width(hs); + return widthpx / SS.GW.scale; +} + +//----------------------------------------------------------------------------- +// Return the associated text height, in pixels. +//----------------------------------------------------------------------------- +double Style::TextHeight(hStyle h) { + Style *s = Get(h); + switch(s->textHeightAs) { + case UnitsAs::MM: return s->textHeight * SS.GW.scale; + case UnitsAs::PIXELS: return s->textHeight; + } + ssassert(false, "Unexpected units"); +} + +double Style::DefaultTextHeight() { + hStyle hs { Style::CONSTRAINT }; + return TextHeight(hs); +} + +//----------------------------------------------------------------------------- +// Return the parameters of this style, as a canvas stroke. +//----------------------------------------------------------------------------- +Canvas::Stroke Style::Stroke(hStyle hs) { + Canvas::Stroke stroke = {}; + Style *style = Style::Get(hs); + stroke.color = style->color; + stroke.stipplePattern = style->stippleType; + stroke.stippleScale = style->stippleScale; + stroke.width = style->width; + switch(style->widthAs) { + case Style::UnitsAs::PIXELS: + stroke.unit = Canvas::Unit::PX; + break; + case Style::UnitsAs::MM: + stroke.unit = Canvas::Unit::MM; + break; + } + return stroke; +} + +Canvas::Stroke Style::Stroke(int hsv) { + hStyle hs = { (uint32_t) hsv }; + return Style::Stroke(hs); +} + +//----------------------------------------------------------------------------- +// Should lines and curves from this style appear in the output file? Only +// if it's both shown and exportable. +//----------------------------------------------------------------------------- +bool Style::Exportable(int si) { + hStyle hs = { (uint32_t)si }; + Style *s = Get(hs); + return (s->exportable) && (s->visible); +} + +//----------------------------------------------------------------------------- +// Return the appropriate style for our entity. If the entity has a style +// explicitly assigned, then it's that style. Otherwise it's the appropriate +// default style. +//----------------------------------------------------------------------------- +hStyle Style::ForEntity(hEntity he) { + Entity *e = SK.GetEntity(he); + // If the entity has a special style, use that. If that style doesn't + // exist yet, then it will get created automatically later. + if(e->style.v != 0) { + return e->style; + } + + // Otherwise, we use the default rules. + hStyle hs; + if(e->group != SS.GW.activeGroup) { + hs.v = INACTIVE_GRP; + } else if(e->construction) { + hs.v = CONSTRUCTION; + } else { + hs.v = ACTIVE_GRP; + } + return hs; +} + +StipplePattern Style::StipplePatternFromString(std::string name) { + std::transform(name.begin(), name.end(), name.begin(), ::tolower); + if(name == "continuous") { + return StipplePattern::CONTINUOUS; + } else if(name == "shortdash") { + return StipplePattern::SHORT_DASH; + } else if(name == "dash") { + return StipplePattern::DASH; + } else if(name == "longdash") { + return StipplePattern::LONG_DASH; + } else if(name == "dashdot") { + return StipplePattern::DASH_DOT; + } else if(name == "dashdotdot") { + return StipplePattern::DASH_DOT_DOT; + } else if(name == "dot") { + return StipplePattern::DOT; + } else if(name == "freehand") { + return StipplePattern::FREEHAND; + } else if(name == "zigzag") { + return StipplePattern::ZIGZAG; + } + + return StipplePattern::CONTINUOUS; +} + +StipplePattern Style::PatternType(hStyle hs) { + Style *s = Get(hs); + return s->stippleType; +} + +std::string Style::StipplePatternName(hStyle hs) { + Style *s = Get(hs); + return StipplePatternName(s->stippleType); +} + +std::string Style::StipplePatternName(StipplePattern stippleType) { + switch(stippleType) { + case StipplePattern::CONTINUOUS: return "Continuous"; + case StipplePattern::SHORT_DASH: return "ShortDash"; + case StipplePattern::DASH: return "Dash"; + case StipplePattern::LONG_DASH: return "LongDash"; + case StipplePattern::DASH_DOT: return "DashDot"; + case StipplePattern::DASH_DOT_DOT: return "DashDotDot"; + case StipplePattern::DOT: return "Dot"; + case StipplePattern::FREEHAND: return "FreeHand"; + case StipplePattern::ZIGZAG: return "ZigZag"; + } + + return "Continuous"; +} + +double Style::StippleScale(hStyle hs) { + Style *s = Get(hs); + return s->stippleScale; +} + +double Style::StippleScaleMm(hStyle hs) { + Style *s = Get(hs); + if(s->widthAs == UnitsAs::MM) { + return s->stippleScale; + } else if(s->widthAs == UnitsAs::PIXELS) { + return s->stippleScale / SS.GW.scale; + } + return 1.0; +} + +std::string Style::DescriptionString() const { + if(name.empty()) { + return ssprintf("s%03x-(unnamed)", h.v); + } else { + return ssprintf("s%03x-%s", h.v, name.c_str()); + } +} + + +void TextWindow::ScreenShowListOfStyles(int link, uint32_t v) { + SS.TW.GoToScreen(Screen::LIST_OF_STYLES); +} +void TextWindow::ScreenShowStyleInfo(int link, uint32_t v) { + GraphicsWindow::MenuEdit(Command::UNSELECT_ALL); + SS.TW.GoToScreen(Screen::STYLE_INFO); + SS.TW.shown.style.v = v; +} + +void TextWindow::ScreenLoadFactoryDefaultStyles(int link, uint32_t v) { + Style::LoadFactoryDefaults(); + SS.TW.GoToScreen(Screen::LIST_OF_STYLES); + SS.GW.persistentDirty = true; +} + +void TextWindow::ScreenCreateCustomStyle(int link, uint32_t v) { + Style::CreateCustomStyle(); +} + +void TextWindow::ScreenChangeBackgroundColor(int link, uint32_t v) { + RgbaColor rgb = SS.backgroundColor; + SS.TW.ShowEditControlWithColorPicker(3, rgb); + SS.TW.edit.meaning = Edit::BACKGROUND_COLOR; +} + +void TextWindow::ShowListOfStyles() { + Printf(true, "%Ft color style-name"); + + bool darkbg = false; + for(Style &s : SK.style) { + Printf(false, "%Bp %Bz %Bp %Fl%Ll%f%D%s%E", + darkbg ? 'd' : 'a', + &s.color, + darkbg ? 'd' : 'a', + ScreenShowStyleInfo, s.h.v, + s.DescriptionString().c_str()); + + darkbg = !darkbg; + } + + Printf(true, " %Fl%Ll%fcreate a new custom style%E", + &ScreenCreateCustomStyle); + + Printf(false, ""); + + RgbaColor rgb = SS.backgroundColor; + Printf(false, "%Ft background color (r, g, b)%E"); + Printf(false, "%Ba %@, %@, %@ %Fl%D%f%Ll[change]%E", + rgb.redF(), rgb.greenF(), rgb.blueF(), + top[rows-1] + 2, &ScreenChangeBackgroundColor); + + Printf(false, ""); + Printf(false, " %Fl%Ll%fload factory defaults%E", + &ScreenLoadFactoryDefaultStyles); +} + + +void TextWindow::ScreenChangeStyleName(int link, uint32_t v) { + hStyle hs = { v }; + Style *s = Style::Get(hs); + SS.TW.ShowEditControl(12, s->name); + SS.TW.edit.style = hs; + SS.TW.edit.meaning = Edit::STYLE_NAME; +} + +void TextWindow::ScreenDeleteStyle(int link, uint32_t v) { + SS.UndoRemember(); + hStyle hs = { v }; + Style *s = SK.style.FindByIdNoOops(hs); + if(s) { + SK.style.RemoveById(hs); + // And it will get recreated automatically if something is still using + // the style, so no need to do anything else. + } + SS.TW.GoToScreen(Screen::LIST_OF_STYLES); + SS.GW.Invalidate(); +} + +void TextWindow::ScreenChangeStylePatternType(int link, uint32_t v) { + hStyle hs = { v }; + Style *s = Style::Get(hs); + s->stippleType = (StipplePattern)(link - 1); + SS.GW.persistentDirty = true; +} + +void TextWindow::ScreenChangeStyleMetric(int link, uint32_t v) { + hStyle hs = { v }; + Style *s = Style::Get(hs); + double val; + Style::UnitsAs units; + Edit meaning; + int col; + switch(link) { + case 't': + val = s->textHeight; + units = s->textHeightAs; + col = 10; + meaning = Edit::STYLE_TEXT_HEIGHT; + break; + + case 's': + val = s->stippleScale; + units = s->widthAs; + col = 17; + meaning = Edit::STYLE_STIPPLE_PERIOD; + break; + + case 'w': + case 'W': + val = s->width; + units = s->widthAs; + col = 9; + meaning = Edit::STYLE_WIDTH; + break; + + default: ssassert(false, "Unexpected link"); + } + + std::string edit_value; + if(units == Style::UnitsAs::PIXELS) { + edit_value = ssprintf("%.2f", val); + } else { + edit_value = SS.MmToString(val, true); + } + SS.TW.ShowEditControl(col, edit_value); + SS.TW.edit.style = hs; + SS.TW.edit.meaning = meaning; +} + +void TextWindow::ScreenChangeStyleTextAngle(int link, uint32_t v) { + hStyle hs = { v }; + Style *s = Style::Get(hs); + SS.TW.ShowEditControl(9, ssprintf("%.2f", s->textAngle)); + SS.TW.edit.style = hs; + SS.TW.edit.meaning = Edit::STYLE_TEXT_ANGLE; +} + +void TextWindow::ScreenChangeStyleColor(int link, uint32_t v) { + hStyle hs = { v }; + Style *s = Style::Get(hs); + // Same function used for stroke and fill colors + Edit em; + RgbaColor rgb; + if(link == 's') { + em = Edit::STYLE_COLOR; + rgb = s->color; + } else if(link == 'f') { + em = Edit::STYLE_FILL_COLOR; + rgb = s->fillColor; + } else ssassert(false, "Unexpected link"); + SS.TW.ShowEditControlWithColorPicker(13, rgb); + SS.TW.edit.style = hs; + SS.TW.edit.meaning = em; +} + +void TextWindow::ScreenChangeStyleYesNo(int link, uint32_t v) { + SS.UndoRemember(); + hStyle hs = { v }; + Style *s = Style::Get(hs); + switch(link) { + // Units for the width + case 'w': + if(s->widthAs != Style::UnitsAs::MM) { + s->widthAs = Style::UnitsAs::MM; + s->width /= SS.GW.scale; + s->stippleScale /= SS.GW.scale; + } + break; + case 'W': + if(s->widthAs != Style::UnitsAs::PIXELS) { + s->widthAs = Style::UnitsAs::PIXELS; + s->width *= SS.GW.scale; + s->stippleScale *= SS.GW.scale; + } + break; + + // Units for the height + case 'g': + if(s->textHeightAs != Style::UnitsAs::MM) { + s->textHeightAs = Style::UnitsAs::MM; + s->textHeight /= SS.GW.scale; + } + break; + + case 'G': + if(s->textHeightAs != Style::UnitsAs::PIXELS) { + s->textHeightAs = Style::UnitsAs::PIXELS; + s->textHeight *= SS.GW.scale; + } + break; + + case 'e': + s->exportable = !(s->exportable); + break; + + case 'v': + s->visible = !(s->visible); + break; + + case 'f': + s->filled = !(s->filled); + break; + + // Horizontal text alignment + case 'L': + s->textOrigin = (Style::TextOrigin)((uint32_t)s->textOrigin | (uint32_t)Style::TextOrigin::LEFT); + s->textOrigin = (Style::TextOrigin)((uint32_t)s->textOrigin & ~(uint32_t)Style::TextOrigin::RIGHT); + break; + case 'H': + s->textOrigin = (Style::TextOrigin)((uint32_t)s->textOrigin & ~(uint32_t)Style::TextOrigin::LEFT); + s->textOrigin = (Style::TextOrigin)((uint32_t)s->textOrigin & ~(uint32_t)Style::TextOrigin::RIGHT); + break; + case 'R': + s->textOrigin = (Style::TextOrigin)((uint32_t)s->textOrigin & ~(uint32_t)Style::TextOrigin::LEFT); + s->textOrigin = (Style::TextOrigin)((uint32_t)s->textOrigin | (uint32_t)Style::TextOrigin::RIGHT); + break; + + // Vertical text alignment + case 'B': + s->textOrigin = (Style::TextOrigin)((uint32_t)s->textOrigin | (uint32_t)Style::TextOrigin::BOT); + s->textOrigin = (Style::TextOrigin)((uint32_t)s->textOrigin & ~(uint32_t)Style::TextOrigin::TOP); + break; + case 'V': + s->textOrigin = (Style::TextOrigin)((uint32_t)s->textOrigin & ~(uint32_t)Style::TextOrigin::BOT); + s->textOrigin = (Style::TextOrigin)((uint32_t)s->textOrigin & ~(uint32_t)Style::TextOrigin::TOP); + break; + case 'T': + s->textOrigin = (Style::TextOrigin)((uint32_t)s->textOrigin & ~(uint32_t)Style::TextOrigin::BOT); + s->textOrigin = (Style::TextOrigin)((uint32_t)s->textOrigin | (uint32_t)Style::TextOrigin::TOP); + break; + } + SS.GW.Invalidate(/*clearPersistent=*/true); +} + +bool TextWindow::EditControlDoneForStyles(const std::string &str) { + Style *s; + switch(edit.meaning) { + case Edit::STYLE_STIPPLE_PERIOD: + case Edit::STYLE_TEXT_HEIGHT: + case Edit::STYLE_WIDTH: { + SS.UndoRemember(); + s = Style::Get(edit.style); + + double v; + Style::UnitsAs units = (edit.meaning == Edit::STYLE_TEXT_HEIGHT) ? + s->textHeightAs : s->widthAs; + if(units == Style::UnitsAs::MM) { + v = SS.StringToMm(str); + } else { + v = atof(str.c_str()); + } + v = max(0.0, v); + if(edit.meaning == Edit::STYLE_TEXT_HEIGHT) { + s->textHeight = v; + } else if(edit.meaning == Edit::STYLE_STIPPLE_PERIOD) { + s->stippleScale = v; + } else { + s->width = v; + } + break; + } + case Edit::STYLE_TEXT_ANGLE: + SS.UndoRemember(); + s = Style::Get(edit.style); + s->textAngle = WRAP_SYMMETRIC(atof(str.c_str()), 360); + break; + + case Edit::BACKGROUND_COLOR: + case Edit::STYLE_FILL_COLOR: + case Edit::STYLE_COLOR: { + Vector rgb; + if(sscanf(str.c_str(), "%lf, %lf, %lf", &rgb.x, &rgb.y, &rgb.z)==3) { + rgb = rgb.ClampWithin(0, 1); + if(edit.meaning == Edit::STYLE_COLOR) { + SS.UndoRemember(); + s = Style::Get(edit.style); + s->color = RGBf(rgb.x, rgb.y, rgb.z); + } else if(edit.meaning == Edit::STYLE_FILL_COLOR) { + SS.UndoRemember(); + s = Style::Get(edit.style); + s->fillColor = RGBf(rgb.x, rgb.y, rgb.z); + } else { + SS.backgroundColor = RGBf(rgb.x, rgb.y, rgb.z); + } + } else { + Error(_("Bad format: specify color as r, g, b")); + } + break; + } + case Edit::STYLE_NAME: + if(str.empty()) { + Error(_("Style name cannot be empty")); + } else { + SS.UndoRemember(); + s = Style::Get(edit.style); + s->name = str; + } + break; + + default: return false; + } + SS.GW.persistentDirty = true; + return true; +} + +void TextWindow::ShowStyleInfo() { + Printf(true, "%Fl%f%Ll(back to list of styles)%E", &ScreenShowListOfStyles); + + Style *s = Style::Get(shown.style); + + if(s->h.v < Style::FIRST_CUSTOM) { + Printf(true, "%FtSTYLE %E%s ", s->DescriptionString().c_str()); + } else { + Printf(true, "%FtSTYLE %E%s " + "[%Fl%Ll%D%frename%E/%Fl%Ll%D%fdel%E]", + s->DescriptionString().c_str(), + s->h.v, &ScreenChangeStyleName, + s->h.v, &ScreenDeleteStyle); + } + Printf(true, "%Ft line stroke style%E"); + Printf(false, "%Ba %Ftcolor %E%Bz %Ba (%@, %@, %@) %D%f%Ls%Fl[change]%E", + &s->color, + s->color.redF(), s->color.greenF(), s->color.blueF(), + s->h.v, ScreenChangeStyleColor); + + // The line width, and its units + if(s->widthAs == Style::UnitsAs::PIXELS) { + Printf(false, " %Ftwidth%E %@ %D%f%Lp%Fl[change]%E", + s->width, + s->h.v, &ScreenChangeStyleMetric, + (s->h.v < Style::FIRST_CUSTOM) ? 'w' : 'W'); + } else { + Printf(false, " %Ftwidth%E %s %D%f%Lp%Fl[change]%E", + SS.MmToString(s->width).c_str(), + s->h.v, &ScreenChangeStyleMetric, + (s->h.v < Style::FIRST_CUSTOM) ? 'w' : 'W'); + } + + if(s->widthAs == Style::UnitsAs::PIXELS) { + Printf(false, "%Ba %Ftstipple width%E %@ %D%f%Lp%Fl[change]%E", + s->stippleScale, + s->h.v, &ScreenChangeStyleMetric, 's'); + } else { + Printf(false, "%Ba %Ftstipple width%E %s %D%f%Lp%Fl[change]%E", + SS.MmToString(s->stippleScale).c_str(), + s->h.v, &ScreenChangeStyleMetric, 's'); + } + + bool widthpx = (s->widthAs == Style::UnitsAs::PIXELS); + if(s->h.v < Style::FIRST_CUSTOM) { + Printf(false," %Ftin units of %Fdpixels%E"); + } else { + Printf(false,"%Ba %Ftin units of %Fd" + "%D%f%LW%s pixels%E " + "%D%f%Lw%s %s", + s->h.v, &ScreenChangeStyleYesNo, + widthpx ? RADIO_TRUE : RADIO_FALSE, + s->h.v, &ScreenChangeStyleYesNo, + !widthpx ? RADIO_TRUE : RADIO_FALSE, + SS.UnitName()); + } + + Printf(false,"%Ba %Ftstipple type:%E"); + + const size_t patternCount = (size_t)StipplePattern::LAST + 1; + const char *patternsSource[patternCount] = { + "___________", + "- - - - ", + "- - - - - -", + "__ __ __ __", + "-.-.-.-.-.-", + "..-..-..-..", + "...........", + "~~~~~~~~~~~", + "__~__~__~__" + }; + std::string patterns[patternCount]; + + for(uint32_t i = 0; i <= (uint32_t)StipplePattern::LAST; i++) { + const char *str = patternsSource[i]; + do { + switch(*str) { + case ' ': patterns[i] += " "; break; + case '.': patterns[i] += "\xEE\x80\x84"; break; + case '_': patterns[i] += "\xEE\x80\x85"; break; + case '-': patterns[i] += "\xEE\x80\x86"; break; + case '~': patterns[i] += "\xEE\x80\x87"; break; + default: ssassert(false, "Unexpected stipple pattern element"); + } + } while(*(++str)); + } + + for(uint32_t i = 0; i <= (uint32_t)StipplePattern::LAST; i++) { + const char *radio = s->stippleType == (StipplePattern)i ? RADIO_TRUE : RADIO_FALSE; + Printf(false, "%Bp %D%f%Lp%s %s%E", + (i % 2 == 0) ? 'd' : 'a', + s->h.v, &ScreenChangeStylePatternType, + i + 1, radio, patterns[i].c_str()); + } + + if(s->h.v >= Style::FIRST_CUSTOM) { + // The fill color, and whether contours are filled + + Printf(false, ""); + Printf(false, "%Ft contour fill style%E"); + Printf(false, + "%Ba %Ftcolor %E%Bz %Ba (%@, %@, %@) %D%f%Lf%Fl[change]%E", + &s->fillColor, + s->fillColor.redF(), s->fillColor.greenF(), s->fillColor.blueF(), + s->h.v, ScreenChangeStyleColor); + + Printf(false, "%Bd %D%f%Lf%s contours are filled%E", + s->h.v, &ScreenChangeStyleYesNo, + s->filled ? CHECK_TRUE : CHECK_FALSE); + } + + // The text height, and its units + Printf(false, ""); + Printf(false, "%Ft text style%E"); + + if(s->textHeightAs == Style::UnitsAs::PIXELS) { + Printf(false, "%Ba %Ftheight %E%@ %D%f%Lt%Fl%s%E", + s->textHeight, + s->h.v, &ScreenChangeStyleMetric, + "[change]"); + } else { + Printf(false, "%Ba %Ftheight %E%s %D%f%Lt%Fl%s%E", + SS.MmToString(s->textHeight).c_str(), + s->h.v, &ScreenChangeStyleMetric, + "[change]"); + } + + bool textHeightpx = (s->textHeightAs == Style::UnitsAs::PIXELS); + if(s->h.v < Style::FIRST_CUSTOM) { + Printf(false,"%Bd %Ftin units of %Fdpixels"); + } else { + Printf(false,"%Bd %Ftin units of %Fd" + "%D%f%LG%s pixels%E " + "%D%f%Lg%s %s", + s->h.v, &ScreenChangeStyleYesNo, + textHeightpx ? RADIO_TRUE : RADIO_FALSE, + s->h.v, &ScreenChangeStyleYesNo, + !textHeightpx ? RADIO_TRUE : RADIO_FALSE, + SS.UnitName()); + } + + if(s->h.v >= Style::FIRST_CUSTOM) { + Printf(false, "%Ba %Ftangle %E%@ %D%f%Ll%Fl[change]%E", + s->textAngle, + s->h.v, &ScreenChangeStyleTextAngle); + + Printf(false, ""); + Printf(false, "%Ft text comment alignment%E"); + bool neither; + neither = !((uint32_t)s->textOrigin & ((uint32_t)Style::TextOrigin::LEFT | (uint32_t)Style::TextOrigin::RIGHT)); + Printf(false, "%Ba " + "%D%f%LL%s left%E " + "%D%f%LH%s center%E " + "%D%f%LR%s right%E ", + s->h.v, &ScreenChangeStyleYesNo, + ((uint32_t)s->textOrigin & (uint32_t)Style::TextOrigin::LEFT) ? RADIO_TRUE : RADIO_FALSE, + s->h.v, &ScreenChangeStyleYesNo, + neither ? RADIO_TRUE : RADIO_FALSE, + s->h.v, &ScreenChangeStyleYesNo, + ((uint32_t)s->textOrigin & (uint32_t)Style::TextOrigin::RIGHT) ? RADIO_TRUE : RADIO_FALSE); + + neither = !((uint32_t)s->textOrigin & ((uint32_t)Style::TextOrigin::BOT | (uint32_t)Style::TextOrigin::TOP)); + Printf(false, "%Bd " + "%D%f%LB%s bottom%E " + "%D%f%LV%s center%E " + "%D%f%LT%s top%E ", + s->h.v, &ScreenChangeStyleYesNo, + ((uint32_t)s->textOrigin & (uint32_t)Style::TextOrigin::BOT) ? RADIO_TRUE : RADIO_FALSE, + s->h.v, &ScreenChangeStyleYesNo, + neither ? RADIO_TRUE : RADIO_FALSE, + s->h.v, &ScreenChangeStyleYesNo, + ((uint32_t)s->textOrigin & (uint32_t)Style::TextOrigin::TOP) ? RADIO_TRUE : RADIO_FALSE); + } + + Printf(false, ""); + + if(s->h.v >= Style::FIRST_CUSTOM) { + Printf(false, " %Fd%D%f%Lv%s show these objects on screen%E", + s->h.v, &ScreenChangeStyleYesNo, + s->visible ? CHECK_TRUE : CHECK_FALSE); + } + + Printf(false, " %Fd%D%f%Le%s export these objects%E", + s->h.v, &ScreenChangeStyleYesNo, + s->exportable ? CHECK_TRUE : CHECK_FALSE); + + if(s->h.v >= Style::FIRST_CUSTOM) { + Printf(false, ""); + Printf(false, "To assign lines or curves to this style,"); + Printf(false, "right-click them on the drawing."); + } +} + +void TextWindow::ScreenAssignSelectionToStyle(int link, uint32_t v) { + Style::AssignSelectionToStyle(v); +} + +} // namespace SolveSpace diff --git a/src/system.cpp b/src/system.cpp new file mode 100644 index 0000000..89d3159 --- /dev/null +++ b/src/system.cpp @@ -0,0 +1,614 @@ +//----------------------------------------------------------------------------- +// Once we've written our constraint equations in the symbolic algebra system, +// these routines linearize them, and solve by a modified Newton's method. +// This also contains the routines to detect non-convergence or inconsistency, +// and report diagnostics to the user. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +#include +#include + +namespace SolveSpace { + +// The solver will converge all unknowns to within this tolerance. This must +// always be much less than LENGTH_EPS, and in practice should be much less. +const double System::CONVERGE_TOLERANCE = (LENGTH_EPS/(1e2)); + +constexpr size_t LikelyPartialCountPerEq = 10; + +bool System::WriteJacobian(int tag) { + // Clear all + mat.param.clear(); + mat.eq.clear(); + mat.A.sym.setZero(); + mat.B.sym.clear(); + + for(Equation &e : eq) { + if(e.tag != tag) continue; + mat.eq.push_back(&e); + } + if(mat.eq.size() >= MAX_UNKNOWNS) { + return false; + } + mat.m = mat.eq.size(); + + std::unordered_map paramToIndex; + for(Param &p : param) { + if(p.tag != tag) continue; + // Fill the param id to index map + paramToIndex[p.h.v] = mat.param.size(); + mat.param.push_back(p.h); + } + mat.n = mat.param.size(); + + // In some experimenting, this is almost always the right size. + // Value is usually between 0 and 20, comes from number of constraints? + mat.A.sym.resize(mat.m, mat.n); + mat.A.sym.reserve(Eigen::VectorXi::Constant(mat.n, LikelyPartialCountPerEq)); + + mat.B.sym.reserve(mat.eq.size()); + for(size_t i = 0; i < mat.eq.size(); i++) { + Equation *e = mat.eq[i]; + // Deep-copy and simplify (fold) the current equation. + Expr *f = e->e->DeepCopyWithParamsAsPointers(¶m, &(SK.param), /*foldConstants=*/true); + + ParamSet paramsUsed; + f->ParamsUsedList(¶msUsed); + + for(hParam p : paramsUsed) { + // Find the index of this parameter + auto it = paramToIndex.find(p.v); + if(it == paramToIndex.end()) continue; + // this is the parameter index + const int j = it->second; + // compute partial derivative of f + Expr *pd = f->PartialWrt(p); + pd = pd->FoldConstants(/*allocCopy=*/false); + if(pd->IsZeroConst()) + continue; + mat.A.sym.insert(i, j) = pd; + } + mat.B.sym.push_back(f); + } + return true; +} + +void System::EvalJacobian() { + using namespace Eigen; + mat.A.num.setZero(); + mat.A.num.resize(mat.m, mat.n); + const int size = mat.A.sym.outerSize(); + + for(int k = 0; k < size; k++) { + for(SparseMatrix ::InnerIterator it(mat.A.sym, k); it; ++it) { + double value = it.value()->Eval(); + if(EXACT(value == 0.0)) continue; + mat.A.num.insert(it.row(), it.col()) = value; + } + } + mat.A.num.makeCompressed(); +} + +bool System::IsDragged(hParam p) { + return dragged.find(p) != dragged.end(); +} + +SubstitutionMap System::SolveBySubstitution() { + // Contains pointers to last substitutions in a substitution chain + std::vector subVec; + // Maps a parameter to the index of its last substitution in `subVec` + std::unordered_map> leaves; + // Tracks how many slots in `subVec` contain a specific last substitution + // (this can happen as slots that once contained a specific substitution + // are updated to point to another over the run of the substitution algorithm) + std::unordered_map, HandleHasher> slotTrack; + + for(auto &teq : eq) { + Expr *tex = teq.e; + + // If we have `(a - b) = 0` where both a and b are parameters, then `a = b` and we can substitute + if(tex->op == Expr::Op::MINUS && + tex->a->op == Expr::Op::PARAM && + tex->b->op == Expr::Op::PARAM) + { + Param *sub = param.FindByIdNoOops(tex->a->parh); + Param *by = param.FindByIdNoOops(tex->b->parh); + if(!sub || !by) { + // Don't substitute unless they're both solver params; + // otherwise it's an equation that can be solved immediately, + // or an error to flag later. + continue; + } + + if(sub->h == by->h) { + teq.tag = EQ_SUBSTITUTED; + continue; + } + + // Take the last substitution of parameter a + size_t subIdx = 0; + auto it = leaves.find(sub->h); + if(it != leaves.end()) { + subIdx = it->second; + sub = subVec.at(it->second - 1); + } + + // Take the last substitution of parameter b + size_t byIdx = 0; + it = leaves.find(by->h); + if(it != leaves.end()) { + byIdx = it->second; + by = subVec.at(it->second - 1); + } + + // If the last substituton of `a` is a dragged param, keep it + // and substitute the other param + if(IsDragged(sub->h)) { + std::swap(sub, by); + std::swap(subIdx, byIdx); + } + + if(subIdx == 0) { + if(byIdx == 0) { + // Neither `sub` nor `by` are in the map, so add them and + // set the target index + subVec.push_back(by); + leaves[by->h] = leaves[sub->h] = subVec.size(); + } else { + // `sub` isn't in the map, but `by` is, so just add `sub` to + // the map with `by` as the target + leaves[sub->h] = byIdx; + } + } else { + // `sub` already exists in the map, so just update any slots + // that point to it as the last substitution to point to `by` + // instead + auto it = slotTrack.find(sub->h); + if(it == slotTrack.end()) { + // There's only this one slot, so just replace it with `by` + subVec[subIdx - 1] = by; + + // If `by` was already in the map, that means we now have + // an additional slot where it resides, so add it to the + // slot tracker + if(byIdx != 0) { + // If `by` is already in the slot tracker, we'll get + // back a vector with at least two elements; otherwise + // this access will add a new item to the slot tracker + // with an empty vector + auto &bySlots = slotTrack[by->h]; + if(bySlots.empty()) { + bySlots.push_back(byIdx); + } + bySlots.push_back(subIdx); + } + } else { + // We have more than one slot pointing to `sub`, so update + // all of the slots to point to `by` + for(size_t i : it->second) { + subVec[i - 1] = by; + } + + // No more slots are pointing to `sub`, so extract the slot list + // and erase `sub` from the tracker + auto subSlots = std::move(it->second); + slotTrack.erase(it); + + // Same as above: this access either gives us an existing vector + // with at least two elements, or creates an empty vector + auto &bySlots = slotTrack[by->h]; + if(bySlots.empty()) { + bySlots = std::move(subSlots); + if(byIdx != 0) { + bySlots.push_back(byIdx); + } + } else { + bySlots.insert(bySlots.end(), subSlots.begin(), subSlots.end()); + } + } + + leaves[by->h] = subIdx; + } + + sub->tag = VAR_SUBSTITUTED; + teq.tag = EQ_SUBSTITUTED; + } + } + + SubstitutionMap subMap; + for(auto &sub : leaves) { + Param *by = subVec[sub.second - 1]; + if(sub.first != by->h) { + subMap[sub.first] = by; + } + } + + // Substitute all the equations + for(auto &req : eq) { + req.e->Substitute(subMap); + } + + return subMap; +} + +//----------------------------------------------------------------------------- +// Calculate the rank of the Jacobian matrix +//----------------------------------------------------------------------------- +int System::CalculateRank() { + using namespace Eigen; + if(mat.n == 0 || mat.m == 0) return 0; + SparseQR , COLAMDOrdering> solver; + solver.compute(mat.A.num); + int result = solver.rank(); + return result; +} + +bool System::TestRank(int *dof, int *rank) { + EvalJacobian(); + int jacobianRank = CalculateRank(); + // We are calculating dof based on real rank, not mat.m. + // Using this approach we can calculate real dof even when redundant is allowed. + if(dof != NULL) *dof = mat.n - jacobianRank; + if(rank) { + *rank = jacobianRank; + } + return jacobianRank == mat.m; +} + +bool System::SolveLinearSystem(const Eigen::SparseMatrix &A, + const Eigen::VectorXd &B, Eigen::VectorXd *X) +{ + if(A.outerSize() == 0) return true; + using namespace Eigen; + SparseQR, COLAMDOrdering> solver; + //SimplicialLDLT> solver; + solver.compute(A); + *X = solver.solve(B); + return (solver.info() == Success); +} + +bool System::SolveLeastSquares() { + using namespace Eigen; + // Scale the columns; this scale weights the parameters for the least + // squares solve, so that we can encourage the solver to make bigger + // changes in some parameters, and smaller in others. + VectorXd scale = VectorXd::Ones(mat.n); + for(int c = 0; c < mat.n; c++) { + if(IsDragged(mat.param[c])) { + // It's least squares, so this parameter doesn't need to be all + // that big to get a large effect. + scale[c] = 1 / 20.0; + } + } + + const int size = mat.A.num.outerSize(); + for(int k = 0; k < size; k++) { + for(SparseMatrix::InnerIterator it(mat.A.num, k); it; ++it) { + it.valueRef() *= scale[it.col()]; + } + } + + SparseMatrix AAt = mat.A.num * mat.A.num.transpose(); + AAt.makeCompressed(); + VectorXd z(mat.n); + + if(!SolveLinearSystem(AAt, mat.B.num, &z)) return false; + + mat.X = mat.A.num.transpose() * z; + + for(int c = 0; c < mat.n; c++) { + mat.X[c] *= scale[c]; + } + return true; +} + +bool System::NewtonSolve() { + + int iter = 0; + bool converged = false; + int i; + + // Evaluate the functions at our operating point. + mat.B.num = Eigen::VectorXd(mat.m); + for(i = 0; i < mat.m; i++) { + mat.B.num[i] = (mat.B.sym[i])->Eval(); + } + do { + // And evaluate the Jacobian at our initial operating point. + EvalJacobian(); + + if(!SolveLeastSquares()) break; + + // Take the Newton step; + // J(x_n) (x_{n+1} - x_n) = 0 - F(x_n) + for(i = 0; i < mat.n; i++) { + Param *p = param.FindById(mat.param[i]); + p->val -= mat.X[i]; + if(IsReasonable(p->val)) { + // Very bad, and clearly not convergent + return false; + } + } + + // Re-evalute the functions, since the params have just changed. + for(i = 0; i < mat.m; i++) { + mat.B.num[i] = (mat.B.sym[i])->Eval(); + if(IsReasonable(mat.B.num[i])) { + // Very bad, and clearly not convergent + return false; + } + } + + // Check for convergence + converged = true; + for(i = 0; i < mat.m; i++) { + if(fabs(mat.B.num[i]) > CONVERGE_TOLERANCE) { + converged = false; + break; + } + } + } while(iter++ < 50 && !converged); + + return converged; +} + +void System::WriteEquationsExceptFor(hConstraint hc, Group *g) { + // Generate all the equations from constraints in this group + for(auto &con : SK.constraint) { + ConstraintBase *c = &con; + if(c->group != g->h) continue; + if(c->h == hc) continue; + + if(c->HasLabel() && c->type != Constraint::Type::COMMENT && + g->allDimsReference) + { + // When all dimensions are reference, we adjust them to display + // the correct value, and then don't generate any equations. + c->ModifyToSatisfy(); + continue; + } + if(g->relaxConstraints && c->type != Constraint::Type::POINTS_COINCIDENT) { + // When the constraints are relaxed, we keep only the point- + // coincident constraints, and the constraints generated by + // the entities and groups. + continue; + } + + c->GenerateEquations(&eq); + } + // And the equations from entities + for(auto &ent : SK.entity) { + EntityBase *e = &ent; + if(e->group != g->h) continue; + + e->GenerateEquations(&eq); + } + // And from the groups themselves + g->GenerateEquations(&eq); +} + +void System::FindWhichToRemoveToFixJacobian(Group *g, List *bad, bool forceDofCheck) { + auto time = GetMilliseconds(); + g->solved.timeout = false; + int a; + + for(a = 0; a < 2; a++) { + for(auto &con : SK.constraint) { + if((GetMilliseconds() - time) > g->solved.findToFixTimeout) { + g->solved.timeout = true; + return; + } + + ConstraintBase *c = &con; + if(c->group != g->h) continue; + if((c->type == Constraint::Type::POINTS_COINCIDENT && a == 0) || + (c->type != Constraint::Type::POINTS_COINCIDENT && a == 1)) + { + // Do the constraints in two passes: first everything but + // the point-coincident constraints, then only those + // constraints (so they appear last in the list). + continue; + } + + param.ClearTags(); + eq.Clear(); + WriteEquationsExceptFor(c->h, g); + eq.ClearTags(); + + // It's a major speedup to solve the easy ones by substitution here, + // and that doesn't break anything. + if(!forceDofCheck) { + SolveBySubstitution(); + } + + WriteJacobian(0); + EvalJacobian(); + + int rank = CalculateRank(); + if(rank == mat.m) { + // We fixed it by removing this constraint + bad->Add(&(c->h)); + } + } + } +} + +SolveResult System::Solve(Group *g, int *dof, List *bad, + bool andFindBad, bool andFindFree, bool forceDofCheck) +{ + WriteEquationsExceptFor(Constraint::NO_CONSTRAINT, g); + + bool rankOk; + + // int x; + // printf("%d equations", eq.n); + // for(x = 0; x < eq.n; x++) { + // printf(" %.3f = %s = 0", eq[x].e->Eval(), eq[x].e->Print().c_str()); + // } + // printf("%d parameters", param.n); + // for(x = 0; x < param.n; x++) { + // printf(" param %08x at %.3f", param[x].h.v, param[x].val); + // } + + // All params and equations are assigned to group zero. + param.ClearTags(); + eq.ClearTags(); + + SubstitutionMap subMap; + + // Since we are suppressing dof calculation or allowing redundant, we + // can't / don't want to catch result of dof checking without substitution + if(g->suppressDofCalculation || g->allowRedundant || !forceDofCheck) { + subMap = SolveBySubstitution(); + } + + // Before solving the big system, see if we can find any equations that + // are soluble alone. This can be a huge speedup. We don't know whether + // the system is consistent yet, but if it isn't then we'll catch that + // later. + int alone = 1; + for(auto &e : eq) { + if(e.tag != 0) + continue; + + hParam hp = e.e->ReferencedParams(¶m); + if(hp == Expr::NO_PARAMS) continue; + if(hp == Expr::MULTIPLE_PARAMS) continue; + + Param *p = param.FindById(hp); + if(p->tag != 0) continue; // let rank test catch inconsistency + + e.tag = alone; + p->tag = alone; + WriteJacobian(alone); + if(!NewtonSolve()) { + // We don't do the rank test, so let's arbitrarily return + // the DIDNT_CONVERGE result here. + rankOk = true; + // Failed to converge, bail out early + goto didnt_converge; + } + alone++; + } + + // Now write the Jacobian for what's left, and do a rank test; that + // tells us if the system is inconsistently constrained. + if(!WriteJacobian(0)) { + return SolveResult::TOO_MANY_UNKNOWNS; + } + // Clear dof value in order to have indication when dof is actually not calculated + if(dof != NULL) *dof = -1; + // We are suppressing or allowing redundant, so we no need to catch unsolveable + redundant + rankOk = (!g->suppressDofCalculation && !g->allowRedundant) ? TestRank(dof) : true; + + // And do the leftovers as one big system + if(!NewtonSolve()) { + goto didnt_converge; + } + + // Here we are want to calculate dof even when redundant is allowed, so just handle suppressing + rankOk = (!g->suppressDofCalculation) ? TestRank(dof) : true; + if(!rankOk) { + if(andFindBad) FindWhichToRemoveToFixJacobian(g, bad, forceDofCheck); + } else { + MarkParamsFree(andFindFree); + } + // System solved correctly, so write the new values back in to the + // main parameter table. + for(auto &p : param) { + auto it = subMap.find(p.h); + double val = it == subMap.end() ? p.val : it->second->val; + + Param *pp = SK.GetParam(p.h); + pp->val = val; + pp->known = true; + pp->free = p.free; + } + return rankOk ? SolveResult::OKAY : SolveResult::REDUNDANT_OKAY; + +didnt_converge: + SK.constraint.ClearTags(); + // Not using range-for here because index is used in additional ways + for(size_t i = 0; i < mat.eq.size(); i++) { + if(fabs(mat.B.num[i]) > CONVERGE_TOLERANCE || IsReasonable(mat.B.num[i])) { + // This constraint is unsatisfied. + if(!mat.eq[i]->h.isFromConstraint()) continue; + + hConstraint hc = mat.eq[i]->h.constraint(); + ConstraintBase *c = SK.constraint.FindByIdNoOops(hc); + if(!c) continue; + // Don't double-show constraints that generated multiple + // unsatisfied equations + if(!c->tag) { + bad->Add(&(c->h)); + c->tag = 1; + } + } + } + + return rankOk ? SolveResult::DIDNT_CONVERGE : SolveResult::REDUNDANT_DIDNT_CONVERGE; +} + +SolveResult System::SolveRank(Group *g, int *rank, int *dof, List *bad, + bool andFindBad, bool andFindFree) +{ + WriteEquationsExceptFor(Constraint::NO_CONSTRAINT, g); + + // All params and equations are assigned to group zero. + param.ClearTags(); + eq.ClearTags(); + + // Now write the Jacobian, and do a rank test; that + // tells us if the system is inconsistently constrained. + if(!WriteJacobian(0)) { + return SolveResult::TOO_MANY_UNKNOWNS; + } + + bool rankOk = TestRank(dof, rank); + if(!rankOk) { + // When we are testing with redundant allowed, we don't want to have additional info + // about redundants since this test is working only for single redundant constraint + if(!g->suppressDofCalculation && !g->allowRedundant) { + if(andFindBad) FindWhichToRemoveToFixJacobian(g, bad, true); + } + } else { + MarkParamsFree(andFindFree); + } + return rankOk ? SolveResult::OKAY : SolveResult::REDUNDANT_OKAY; +} + +void System::Clear() { + entity.Clear(); + param.Clear(); + eq.Clear(); + dragged.clear(); + mat.A.num.setZero(); + mat.A.sym.setZero(); +} + +void System::MarkParamsFree(bool find) { + // If requested, find all the free (unbound) variables. This might be + // more than the number of degrees of freedom. Don't always do this, + // because the display would get annoying and it's slow. + for(auto &p : param) { + p.free = false; + + if(find) { + if(p.tag == 0) { + p.tag = VAR_DOF_TEST; + WriteJacobian(0); + EvalJacobian(); + int rank = CalculateRank(); + if(rank == mat.m) { + p.free = true; + } + p.tag = 0; + } + } + } +} + +} // namespace SolveSpace diff --git a/src/textscreens.cpp b/src/textscreens.cpp new file mode 100644 index 0000000..41dcb1b --- /dev/null +++ b/src/textscreens.cpp @@ -0,0 +1,984 @@ +//----------------------------------------------------------------------------- +// The text-based browser window, used to view the structure of the model by +// groups and for other similar purposes. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +//----------------------------------------------------------------------------- +// A navigation bar that always appears at the top of the window, with a +// link to bring us back home. +//----------------------------------------------------------------------------- +void TextWindow::ScreenHome(int link, uint32_t v) { + SS.TW.GoToScreen(Screen::LIST_OF_GROUPS); +} +void TextWindow::ShowHeader(bool withNav) { + ClearScreen(); + + const char *header; + std::string desc; + if(SS.GW.LockedInWorkplane()) { + header = "in plane: "; + desc = SK.GetEntity(SS.GW.ActiveWorkplane())->DescriptionString(); + } else { + header = "drawing / constraining in 3d"; + desc = ""; + } + + // Navigation buttons + if(withNav) { + Printf(false, " %Fl%Lh%fhome%E %Ft%s%E%s", + (&TextWindow::ScreenHome), header, desc.c_str()); + } else { + Printf(false, " %Ft%s%E%s", header, desc.c_str()); + } + + // Leave space for the icons that are painted here. + Printf(false, ""); + Printf(false, ""); +} + +//----------------------------------------------------------------------------- +// The screen that shows a list of every group in the sketch, with options +// to hide or show them, and to view them in detail. This is our home page. +//----------------------------------------------------------------------------- +void TextWindow::ScreenSelectGroup(int link, uint32_t v) { + GraphicsWindow::MenuEdit(Command::UNSELECT_ALL); + SS.TW.GoToScreen(Screen::GROUP_INFO); + SS.TW.shown.group.v = v; +} +void TextWindow::ScreenToggleGroupShown(int link, uint32_t v) { + hGroup hg = { v }; + Group *g = SK.GetGroup(hg); + g->visible = !(g->visible); + // If a group was just shown, then it might not have been generated + // previously, so regenerate. + SS.GenerateAll(); +} +void TextWindow::ScreenShowGroupsSpecial(int link, uint32_t v) { + for(hGroup hg : SK.groupOrder) { + Group *g = SK.GetGroup(hg); + switch(link) { + case 's': g->visible = true; break; + case 'c': g->visible = g->solved.dof != 0; break; + case 'h': g->visible = false; break; + } + } + SS.GW.persistentDirty = true; +} +void TextWindow::ScreenActivateGroup(int link, uint32_t v) { + SS.GW.activeGroup.v = v; + SK.GetGroup(SS.GW.activeGroup)->Activate(); + SS.GW.ClearSuper(); +} +void TextWindow::ReportHowGroupSolved(hGroup hg) { + SS.GW.ClearSuper(); + SS.TW.GoToScreen(Screen::GROUP_SOLVE_INFO); + SS.TW.shown.group = hg; + SS.ScheduleShowTW(); +} +void TextWindow::ScreenHowGroupSolved(int link, uint32_t v) { + if(SS.GW.activeGroup.v != v) { + ScreenActivateGroup(link, v); + } + SS.TW.GoToScreen(Screen::GROUP_SOLVE_INFO); + SS.TW.shown.group.v = v; +} +void TextWindow::ScreenShowConfiguration(int link, uint32_t v) { + SS.TW.GoToScreen(Screen::CONFIGURATION); +} +void TextWindow::ScreenShowEditView(int link, uint32_t v) { + SS.TW.GoToScreen(Screen::EDIT_VIEW); +} +void TextWindow::ScreenGoToWebsite(int link, uint32_t v) { + Platform::OpenInBrowser("http://solvespace.com/txtlink"); +} +void TextWindow::ShowListOfGroups() { + const char *radioTrue = " " RADIO_TRUE " ", + *radioFalse = " " RADIO_FALSE " ", + *checkTrue = " " CHECK_TRUE " ", + *checkFalse = " " CHECK_FALSE " "; + + Printf(true, "%Ft active"); + Printf(false, "%Ft shown dof group-name%E"); + bool afterActive = false; + bool backgroundParity = false; + for(hGroup hg : SK.groupOrder) { + Group *g = SK.GetGroup(hg); + + std::string s = g->DescriptionString(); + bool active = (g->h == SS.GW.activeGroup); + bool shown = g->visible; + bool ok = g->IsSolvedOkay(); + bool warn = (g->type == Group::Type::DRAWING_WORKPLANE && + g->polyError.how != PolyError::GOOD) || + ((g->type == Group::Type::EXTRUDE || + g->type == Group::Type::LATHE) && + SK.GetGroup(g->opA)->polyError.how != PolyError::GOOD); + int dof = g->solved.dof; + char sdof[16] = "ok "; + if(ok && dof > 0) { + if(dof > 999) { + strcpy(sdof, "###"); + } else { + sprintf(sdof, "%-3d", dof); + } + } + std::string suffix; + if(g->forceToMesh || g->IsTriangleMeshAssembly()) { + suffix = " (∆)"; + } + + bool ref = (g->h == Group::HGROUP_REFERENCES); + Printf(false, + "%Bp%Fd " + "%Ft%s%Fb%D%f%Ll%s%E " + "%Fb%s%D%f%Ll%s%E " + "%Fp%D%f%s%Ll%s%E " + "%Fp%Ll%D%f%s%E%s", + // Alternate between light and dark backgrounds, for readability + backgroundParity ? 'd' : 'a', + // Link that activates the group + ref ? " " : "", + g->h.v, (&TextWindow::ScreenActivateGroup), + ref ? "" : (active ? radioTrue : radioFalse), + // Link that hides or shows the group + afterActive ? " - " : "", + g->h.v, (&TextWindow::ScreenToggleGroupShown), + afterActive ? "" : (shown ? checkTrue : checkFalse), + // Link to the errors, if a problem occurred while solving + ok ? (warn ? 'm' : (dof > 0 ? 'i' : 's')) : 'x', + g->h.v, (&TextWindow::ScreenHowGroupSolved), + ok ? ((warn && SS.checkClosedContour) ? "err" : sdof) : "", + ok ? "" : "ERR", + // Link to a screen that gives more details on the group + g->suppress ? 'g' : 'l', + g->h.v, (&TextWindow::ScreenSelectGroup), s.c_str(), + suffix.c_str()); + + if(active) afterActive = true; + backgroundParity = !backgroundParity; + } + + Printf(true, " %Fl%Ls%fshow all%E / %Fl%Lc%fonly unconstrained%E / %Fl%Lh%fhide all%E", + &(TextWindow::ScreenShowGroupsSpecial), + &(TextWindow::ScreenShowGroupsSpecial), + &(TextWindow::ScreenShowGroupsSpecial)); + Printf(true, " %Fl%Ls%fline styles%E /" + " %Fl%Ls%fview%E /" + " %Fl%Ls%fconfiguration%E", + &(TextWindow::ScreenShowListOfStyles), + &(TextWindow::ScreenShowEditView), + &(TextWindow::ScreenShowConfiguration)); +} + + +//----------------------------------------------------------------------------- +// The screen that shows information about a specific group, and allows the +// user to edit various things about it. +//----------------------------------------------------------------------------- +void TextWindow::ScreenHoverGroupWorkplane(int link, uint32_t v) { + SS.GW.hover.Clear(); + hGroup hg = { v }; + SS.GW.hover.entity = hg.entity(0); + SS.GW.hover.emphasized = true; +} +void TextWindow::ScreenHoverEntity(int link, uint32_t v) { + SS.GW.hover.Clear(); + hEntity he = { v }; + SS.GW.hover.entity = he; + SS.GW.hover.emphasized = true; +} +void TextWindow::ScreenHoverRequest(int link, uint32_t v) { + SS.GW.hover.Clear(); + hRequest hr = { v }; + SS.GW.hover.entity = hr.entity(0); + SS.GW.hover.emphasized = true; +} +void TextWindow::ScreenHoverConstraint(int link, uint32_t v) { + if( SS.GW.showConstraints == GraphicsWindow::ShowConstraintMode::SCM_NOSHOW ) return; + hConstraint hc = { v }; + SS.GW.hover.Clear(); + SS.GW.hover.constraint = hc; + SS.GW.hover.emphasized = true; +} +void TextWindow::ScreenSelectEntity(int link, uint32_t v) { + SS.GW.ClearSelection(); + GraphicsWindow::Selection sel = {}; + hEntity he = { v }; + sel.entity = he; + SS.GW.selection.Add(&sel); +} +void TextWindow::ScreenSelectRequest(int link, uint32_t v) { + SS.GW.ClearSelection(); + GraphicsWindow::Selection sel = {}; + hRequest hr = { v }; + sel.entity = hr.entity(0); + SS.GW.selection.Add(&sel); +} +void TextWindow::ScreenSelectConstraint(int link, uint32_t v) { + SS.GW.ClearSelection(); + GraphicsWindow::Selection sel = {}; + sel.constraint.v = v; + SS.GW.selection.Add(&sel); +} + +void TextWindow::ScreenChangeGroupOption(int link, uint32_t v) { + SS.UndoRemember(); + Group *g = SK.GetGroup(SS.TW.shown.group); + + switch(link) { + case 's': + if(g->subtype == Group::Subtype::TWO_SIDED) + g->subtype = Group::Subtype::ONE_SIDED; + if(g->subtype == Group::Subtype::TWO_SKEWED) + g->subtype = Group::Subtype::ONE_SKEWED; + break; + case 'S': + if(g->subtype == Group::Subtype::ONE_SIDED) + g->subtype = Group::Subtype::TWO_SIDED; + if(g->subtype == Group::Subtype::ONE_SKEWED) + g->subtype = Group::Subtype::TWO_SKEWED; + break; + case 'w': + if(g->subtype == Group::Subtype::ONE_SIDED) { + g->subtype = Group::Subtype::ONE_SKEWED; + break; + } + if(g->subtype == Group::Subtype::TWO_SIDED) { + g->subtype = Group::Subtype::TWO_SKEWED; + break; + } + if(g->subtype == Group::Subtype::ONE_SKEWED) { + g->subtype = Group::Subtype::ONE_SIDED; + break; + } + if(g->subtype == Group::Subtype::TWO_SKEWED) { + g->subtype = Group::Subtype::TWO_SIDED; + break; + } + break; + + case 'k': g->skipFirst = true; break; + case 'K': g->skipFirst = false; break; + + case 'c': + if(g->type == Group::Type::EXTRUDE) { + // When an extrude group is first created, it's positioned for a union + // extrusion. If no constraints were added, flip it when we switch between + // union/assemble and difference/intersection modes to avoid manual work doing the same. + if(g->meshCombine != (Group::CombineAs)v && g->GetNumConstraints() == 0) { + // I apologise for this if statement + if((((Group::CombineAs::DIFFERENCE == g->meshCombine) || + (Group::CombineAs::INTERSECTION == g->meshCombine)) && + (Group::CombineAs::DIFFERENCE != (Group::CombineAs)v) && + (Group::CombineAs::INTERSECTION != (Group::CombineAs)v)) || + ((Group::CombineAs::DIFFERENCE != g->meshCombine) && + (Group::CombineAs::INTERSECTION != g->meshCombine) && + ((Group::CombineAs::DIFFERENCE == (Group::CombineAs)v) || + (Group::CombineAs::INTERSECTION == (Group::CombineAs)v)))) { + g->ExtrusionForceVectorTo(g->ExtrusionGetVector().Negated()); + } + } + } + g->meshCombine = (Group::CombineAs)v; + break; + + case 'P': g->suppress = !(g->suppress); break; + + case 'r': g->relaxConstraints = !(g->relaxConstraints); break; + + case 'e': g->allowRedundant = !(g->allowRedundant); break; + + case 'D': g->suppressDofCalculation = !(g->suppressDofCalculation); break; + + case 'v': g->visible = !(g->visible); break; + + case 'd': g->allDimsReference = !(g->allDimsReference); break; + + case 'f': g->forceToMesh = !(g->forceToMesh); break; + } + + SS.MarkGroupDirty(g->h); + SS.GW.ClearSuper(); +} + +void TextWindow::ScreenColor(int link, uint32_t v) { + SS.UndoRemember(); + + Group *g = SK.GetGroup(SS.TW.shown.group); + SS.TW.ShowEditControlWithColorPicker(3, g->color); + SS.TW.edit.meaning = Edit::GROUP_COLOR; +} +void TextWindow::ScreenOpacity(int link, uint32_t v) { + Group *g = SK.GetGroup(SS.TW.shown.group); + + SS.TW.ShowEditControl(11, ssprintf("%.2f", g->color.alphaF())); + SS.TW.edit.meaning = Edit::GROUP_OPACITY; + SS.TW.edit.group = g->h; +} +void TextWindow::ScreenChangeExprA(int link, uint32_t v) { + Group *g = SK.GetGroup(SS.TW.shown.group); + + SS.TW.ShowEditControl(10, ssprintf("%d", (int)g->valA)); + SS.TW.edit.meaning = Edit::TIMES_REPEATED; + SS.TW.edit.group.v = v; +} +void TextWindow::ScreenChangeGroupName(int link, uint32_t v) { + Group *g = SK.GetGroup(SS.TW.shown.group); + SS.TW.ShowEditControl(12, g->DescriptionString().substr(5)); + SS.TW.edit.meaning = Edit::GROUP_NAME; + SS.TW.edit.group.v = v; +} +void TextWindow::ScreenChangeGroupScale(int link, uint32_t v) { + Group *g = SK.GetGroup(SS.TW.shown.group); + + SS.TW.ShowEditControl(13, ssprintf("%.3f", g->scale)); + SS.TW.edit.meaning = Edit::GROUP_SCALE; + SS.TW.edit.group.v = v; +} +void TextWindow::ScreenChangeHelixPitch(int link, uint32_t v) { + Group *g = SK.GetGroup(SS.TW.shown.group); + double pitch = g->valB/SS.MmPerUnit(); + SS.TW.ShowEditControl(3, ssprintf("%.8f", pitch)); + SS.TW.edit.meaning = Edit::HELIX_PITCH; + SS.TW.edit.group.v = v; +} +void TextWindow::ScreenChangePitchOption(int link, uint32_t v) { + Group *g = SK.GetGroup(SS.TW.shown.group); + if(g->valB == 0.0) { + g->valB = SK.GetParam(g->h.param(7))->val * PI / + (SK.GetParam(g->h.param(3))->val); + } else { + g->valB = 0.0; + } + SS.GW.Invalidate(); +} +void TextWindow::ScreenDeleteGroup(int link, uint32_t v) { + SS.UndoRemember(); + + hGroup hg = SS.TW.shown.group; + if(hg == SS.GW.activeGroup) { + SS.GW.activeGroup = SK.GetGroup(SS.GW.activeGroup)->PreviousGroup()->h; + } + + // Reset the text window, since we're displaying information about + // the group that's about to get deleted. + SS.TW.ClearSuper(); + + // This is a major change, so let's re-solve everything. + SK.group.RemoveById(hg); + SS.GenerateAll(SolveSpaceUI::Generate::ALL); + + // Reset the graphics window. This will also recreate the default + // group if it was removed. + SS.GW.ClearSuper(); +} +void TextWindow::ShowGroupInfo() { + Group *g = SK.GetGroup(shown.group); + const char *s = "???"; + + if(shown.group == Group::HGROUP_REFERENCES) { + Printf(true, "%FtGROUP %E%s", g->DescriptionString().c_str()); + goto list_items; + } else { + Printf(true, "%FtGROUP %E%s [%Fl%Ll%D%frename%E/%Fl%Ll%D%fdel%E]", + g->DescriptionString().c_str(), + g->h.v, &TextWindow::ScreenChangeGroupName, + g->h.v, &TextWindow::ScreenDeleteGroup); + } + + if(g->type == Group::Type::LATHE) { + Printf(true, " %Ftlathe plane sketch"); + } else if(g->type == Group::Type::EXTRUDE || g->type == Group::Type::ROTATE || + g->type == Group::Type::TRANSLATE || g->type == Group::Type::REVOLVE || + g->type == Group::Type::HELIX) { + if(g->type == Group::Type::EXTRUDE) { + s = "extrude plane sketch"; + } else if(g->type == Group::Type::TRANSLATE) { + s = "translate original sketch"; + } else if(g->type == Group::Type::HELIX) { + s = "create helical extrusion"; + } else if(g->type == Group::Type::ROTATE) { + s = "rotate original sketch"; + } else if(g->type == Group::Type::REVOLVE) { + s = "revolve original sketch"; + } + Printf(true, " %Ft%s%E", s); + + bool one = ((g->subtype == Group::Subtype::ONE_SIDED) || + (g->subtype == Group::Subtype::ONE_SKEWED)); + bool two = ((g->subtype == Group::Subtype::TWO_SIDED) || + (g->subtype == Group::Subtype::TWO_SKEWED)); + bool skew = ((g->subtype == Group::Subtype::ONE_SKEWED) || + (g->subtype == Group::Subtype::TWO_SKEWED)); + + if (g->type == Group::Type::EXTRUDE) { + Printf(false, + "%Ba %f%Ls%Fd%s one-sided%E " + "%f%LS%Fd%s two-sided%E " + "%f%Lw%Fd%s skewed%E", + &TextWindow::ScreenChangeGroupOption, + one ? RADIO_TRUE : RADIO_FALSE, + &TextWindow::ScreenChangeGroupOption, + two ? RADIO_TRUE : RADIO_FALSE, + &TextWindow::ScreenChangeGroupOption, + skew ? CHECK_TRUE : CHECK_FALSE); + } else { + Printf(false, + "%Ba %f%Ls%Fd%s one-sided%E " + "%f%LS%Fd%s two-sided%E", + &TextWindow::ScreenChangeGroupOption, + one ? RADIO_TRUE : RADIO_FALSE, + &TextWindow::ScreenChangeGroupOption, + two ? RADIO_TRUE : RADIO_FALSE); + } + + if(g->type == Group::Type::ROTATE || g->type == Group::Type::TRANSLATE) { + if(g->subtype == Group::Subtype::ONE_SIDED) { + bool skip = g->skipFirst; + Printf(false, + "%Bd %Ftstart %f%LK%Fd%s with original%E " + "%f%Lk%Fd%s with copy #1%E", + &ScreenChangeGroupOption, + !skip ? RADIO_TRUE : RADIO_FALSE, + &ScreenChangeGroupOption, + skip ? RADIO_TRUE : RADIO_FALSE); + } + + int times = (int)(g->valA); + Printf(false, "%Bp %Ftrepeat%E %d time%s %Fl%Ll%D%f[change]%E", + (g->subtype == Group::Subtype::ONE_SIDED) ? 'a' : 'd', + times, times == 1 ? "" : "s", + g->h.v, &TextWindow::ScreenChangeExprA); + } + } else if(g->type == Group::Type::LINKED) { + Printf(true, " %Ftlink geometry from file%E"); + Platform::Path relativePath = g->linkFile.RelativeTo(SS.saveFile.Parent()); + if(relativePath.IsEmpty()) { + Printf(false, "%Ba '%s'", g->linkFile.raw.c_str()); + } else { + Printf(false, "%Ba '%s'", relativePath.raw.c_str()); + } + Printf(false, "%Bd %Ftscaled by%E %# %Fl%Ll%f%D[change]%E", + g->scale, + &TextWindow::ScreenChangeGroupScale, g->h.v); + } else if(g->type == Group::Type::DRAWING_3D) { + Printf(true, " %Ftsketch in 3d%E"); + } else if(g->type == Group::Type::DRAWING_WORKPLANE) { + Printf(true, " %Ftsketch in new workplane%E"); + } else { + Printf(true, "???"); + } + Printf(false, ""); + + if(g->type == Group::Type::HELIX) { + Printf(false, "%Ft pitch - length per turn%E"); + + if (fabs(g->valB) != 0.0) { + Printf(false, " %Ba %# %Fl%Ll%f%D[change]%E", + g->valB / SS.MmPerUnit(), + &TextWindow::ScreenChangeHelixPitch, g->h.v); + } else { + Printf(false, " %Ba %# %E", + SK.GetParam(g->h.param(7))->val * PI / + ( (SK.GetParam(g->h.param(3))->val) * SS.MmPerUnit() ), + &TextWindow::ScreenChangeHelixPitch, g->h.v); + } + Printf(false, " %Fd%f%LP%s fixed", + &TextWindow::ScreenChangePitchOption, + g->valB != 0 ? CHECK_TRUE : CHECK_FALSE); + + Printf(false, ""); // blank line + } + + if(g->type == Group::Type::EXTRUDE || g->type == Group::Type::LATHE || + g->type == Group::Type::REVOLVE || g->type == Group::Type::LINKED || + g->type == Group::Type::HELIX) { + bool un = (g->meshCombine == Group::CombineAs::UNION); + bool diff = (g->meshCombine == Group::CombineAs::DIFFERENCE); + bool intr = (g->meshCombine == Group::CombineAs::INTERSECTION); + bool asy = (g->meshCombine == Group::CombineAs::ASSEMBLE); + + Printf(false, " %Ftsolid model as"); + Printf(false, "%Ba %f%D%Lc%Fd%s union%E " + "%f%D%Lc%Fd%s assemble%E ", + &TextWindow::ScreenChangeGroupOption, + Group::CombineAs::UNION, + un ? RADIO_TRUE : RADIO_FALSE, + &TextWindow::ScreenChangeGroupOption, + Group::CombineAs::ASSEMBLE, + (asy ? RADIO_TRUE : RADIO_FALSE)); + Printf(false, "%Ba %f%D%Lc%Fd%s difference%E " + "%f%D%Lc%Fd%s intersection%E ", + &TextWindow::ScreenChangeGroupOption, + Group::CombineAs::DIFFERENCE, + diff ? RADIO_TRUE : RADIO_FALSE, + &TextWindow::ScreenChangeGroupOption, + Group::CombineAs::INTERSECTION, + intr ? RADIO_TRUE : RADIO_FALSE); + + if(g->type == Group::Type::EXTRUDE || g->type == Group::Type::LATHE || + g->type == Group::Type::REVOLVE || g->type == Group::Type::HELIX) { + Printf(false, + "%Bd %Ftcolor %E%Bz %Bd (%@, %@, %@) %f%D%Lf%Fl[change]%E", + &g->color, + g->color.redF(), g->color.greenF(), g->color.blueF(), + ScreenColor, top[rows-1] + 2); + Printf(false, "%Bd %Ftopacity%E %@ %f%Lf%Fl[change]%E", + g->color.alphaF(), + &TextWindow::ScreenOpacity); + } + + if(g->type == Group::Type::EXTRUDE || g->type == Group::Type::LATHE || + g->type == Group::Type::REVOLVE || g->type == Group::Type::LINKED || + g->type == Group::Type::HELIX) { + Printf(false, " %Fd%f%LP%s suppress this group's solid model", + &TextWindow::ScreenChangeGroupOption, + g->suppress ? CHECK_TRUE : CHECK_FALSE); + } + + Printf(false, ""); + } + + Printf(false, " %f%Lv%Fd%s show entities from this group", + &TextWindow::ScreenChangeGroupOption, + g->visible ? CHECK_TRUE : CHECK_FALSE); + + if(!g->IsForcedToMeshBySource() && !g->IsTriangleMeshAssembly()) { + Printf(false, " %f%Lf%Fd%s force NURBS surfaces to triangle mesh", + &TextWindow::ScreenChangeGroupOption, + g->forceToMesh ? CHECK_TRUE : CHECK_FALSE); + } else { + Printf(false, " (model already forced to triangle mesh)"); + } + + Printf(true, " %f%Lr%Fd%s relax constraints and dimensions", + &TextWindow::ScreenChangeGroupOption, + g->relaxConstraints ? CHECK_TRUE : CHECK_FALSE); + + Printf(false, " %f%Le%Fd%s allow redundant constraints", + &TextWindow::ScreenChangeGroupOption, + g->allowRedundant ? CHECK_TRUE : CHECK_FALSE); + + Printf(false, " %f%LD%Fd%s suppress dof calculation (improves solver performance)", + &TextWindow::ScreenChangeGroupOption, + g->suppressDofCalculation ? CHECK_TRUE : CHECK_FALSE); + + Printf(false, " %f%Ld%Fd%s treat all dimensions as reference", + &TextWindow::ScreenChangeGroupOption, + g->allDimsReference ? CHECK_TRUE : CHECK_FALSE); + + if(g->booleanFailed) { + Printf(false, ""); + Printf(false, "The Boolean operation failed. It may be "); + Printf(false, "possible to fix the problem by choosing "); + Printf(false, "'force NURBS surfaces to triangle mesh'."); + } + +list_items: + Printf(false, ""); + Printf(false, "%Ft requests in group"); + + int a = 0; + for(auto &r : SK.request) { + + if(r.group == shown.group) { + std::string s = r.DescriptionString(); + Printf(false, "%Bp %Fl%Ll%D%f%h%s%E", + (a & 1) ? 'd' : 'a', + r.h.v, + (&TextWindow::ScreenSelectRequest), + &(TextWindow::ScreenHoverRequest), + s.c_str()); + a++; + } + } + if(a == 0) Printf(false, "%Ba (none)"); + + a = 0; + Printf(false, ""); + Printf(false, "%Ft constraints in group (%d DOF)", g->solved.dof); + for(auto &c : SK.constraint) { + + if(c.group == shown.group) { + std::string s = c.DescriptionString(); + Printf(false, "%Bp %Fl%Ll%D%f%h%s%E %s", + (a & 1) ? 'd' : 'a', + c.h.v, + (&TextWindow::ScreenSelectConstraint), + (&TextWindow::ScreenHoverConstraint), + s.c_str(), + c.reference ? "(ref)" : ""); + a++; + } + } + if(a == 0) Printf(false, "%Ba (none)"); +} + +//----------------------------------------------------------------------------- +// The screen that's displayed when the sketch fails to solve. A report of +// what failed, and (if the problem is a singular Jacobian) a list of +// constraints that could be removed to fix it. +//----------------------------------------------------------------------------- +void TextWindow::ScreenAllowRedundant(int link, uint32_t v) { + SS.UndoRemember(); + + Group *g = SK.GetGroup(SS.TW.shown.group); + g->allowRedundant = true; + SS.MarkGroupDirty(SS.TW.shown.group); + + SS.TW.shown.screen = Screen::GROUP_INFO; + SS.TW.Show(); +} +void TextWindow::ShowGroupSolveInfo() { + Group *g = SK.GetGroup(shown.group); + if(g->IsSolvedOkay()) { + // Go back to the default group info screen + shown.screen = Screen::GROUP_INFO; + Show(); + return; + } + + Printf(true, "%FtGROUP %E%s", g->DescriptionString().c_str()); + switch(g->solved.how) { + case SolveResult::DIDNT_CONVERGE: + Printf(true, "%FxSOLVE FAILED!%Fd unsolvable constraints"); + Printf(true, "the following constraints are incompatible"); + break; + + case SolveResult::REDUNDANT_DIDNT_CONVERGE: + Printf(true, "%FxSOLVE FAILED!%Fd unsolvable constraints"); + Printf(true, "the following constraints are unsatisfied"); + break; + + case SolveResult::REDUNDANT_OKAY: + Printf(true, "%FxSOLVE FAILED!%Fd redundant constraints"); + Printf(true, "remove any one of these to fix it"); + break; + + case SolveResult::TOO_MANY_UNKNOWNS: + Printf(true, "Too many unknowns in a single group!"); + return; + + default: ssassert(false, "Unexpected solve result"); + } + + for(int i = 0; i < g->solved.remove.n; i++) { + hConstraint hc = g->solved.remove[i]; + Constraint *c = SK.constraint.FindByIdNoOops(hc); + if(!c) continue; + + Printf(false, "%Bp %Fl%Ll%D%f%h%s%E", + (i & 1) ? 'd' : 'a', + c->h.v, (&TextWindow::ScreenSelectConstraint), + (&TextWindow::ScreenHoverConstraint), + c->DescriptionString().c_str()); + } + + if(g->solved.timeout) { + Printf(true, "%FxSome items in list have been omitted%Fd"); + Printf(false, "%Fxbecause the operation timed out.%Fd"); + } + + Printf(true, "It may be possible to fix the problem "); + Printf(false, "by selecting Edit -> Undo."); + + if(g->solved.how == SolveResult::REDUNDANT_OKAY) { + Printf(true, "It is possible to suppress this error "); + Printf(false, "by %Fl%f%Llallowing redundant constraints%E in ", + &TextWindow::ScreenAllowRedundant); + Printf(false, "this group."); + } +} + +//----------------------------------------------------------------------------- +// When we're stepping a dimension. User specifies the finish value, and +// how many steps to take in between current and finish, re-solving each +// time. +//----------------------------------------------------------------------------- +void TextWindow::ScreenStepDimFinish(int link, uint32_t v) { + SS.TW.edit.meaning = Edit::STEP_DIM_FINISH; + std::string edit_value; + if(SS.TW.stepDim.isDistance) { + edit_value = SS.MmToString(SS.TW.stepDim.finish, true); + } else { + edit_value = ssprintf("%.3f", SS.TW.stepDim.finish); + } + SS.TW.ShowEditControl(12, edit_value); +} +void TextWindow::ScreenStepDimSteps(int link, uint32_t v) { + SS.TW.edit.meaning = Edit::STEP_DIM_STEPS; + SS.TW.ShowEditControl(12, ssprintf("%d", SS.TW.stepDim.steps)); +} +void TextWindow::ScreenStepDimGo(int link, uint32_t v) { + hConstraint hc = SS.TW.shown.constraint; + Constraint *c = SK.constraint.FindByIdNoOops(hc); + if(c) { + SS.UndoRemember(); + + double start = c->valA, finish = SS.TW.stepDim.finish; + SS.TW.stepDim.time = GetMilliseconds(); + SS.TW.stepDim.step = 1; + + if(!SS.TW.stepDim.timer) { + SS.TW.stepDim.timer = Platform::CreateTimer(); + } + SS.TW.stepDim.timer->onTimeout = [=] { + if(SS.TW.stepDim.step <= SS.TW.stepDim.steps) { + c->valA = start + ((finish - start)*SS.TW.stepDim.step)/SS.TW.stepDim.steps; + SS.MarkGroupDirty(c->group); + SS.GenerateAll(); + if(!SS.ActiveGroupsOkay()) { + // Failed to solve, so quit + return; + } + SS.TW.stepDim.step++; + + const int64_t STEP_MILLIS = 50; + int64_t time = GetMilliseconds(); + if(time - SS.TW.stepDim.time < STEP_MILLIS) { + SS.TW.stepDim.timer->RunAfterNextFrame(); + } else { + SS.TW.stepDim.timer->RunAfter((unsigned)(time - SS.TW.stepDim.time - STEP_MILLIS)); + } + SS.TW.stepDim.time = time; + } else { + SS.TW.GoToScreen(Screen::LIST_OF_GROUPS); + SS.ScheduleShowTW(); + } + SS.GW.Invalidate(); + }; + SS.TW.stepDim.timer->RunAfterNextFrame(); + } +} +void TextWindow::ShowStepDimension() { + Constraint *c = SK.constraint.FindByIdNoOops(shown.constraint); + if(!c) { + shown.screen = Screen::LIST_OF_GROUPS; + Show(); + return; + } + + Printf(true, "%FtSTEP DIMENSION%E %s", c->DescriptionString().c_str()); + + if(stepDim.isDistance) { + Printf(true, "%Ba %Ftstart%E %s", SS.MmToString(c->valA).c_str()); + Printf(false, "%Bd %Ftfinish%E %s %Fl%Ll%f[change]%E", + SS.MmToString(stepDim.finish).c_str(), &ScreenStepDimFinish); + } else { + Printf(true, "%Ba %Ftstart%E %@", c->valA); + Printf(false, "%Bd %Ftfinish%E %@ %Fl%Ll%f[change]%E", + stepDim.finish, &ScreenStepDimFinish); + } + Printf(false, "%Ba %Ftsteps%E %d %Fl%Ll%f%D[change]%E", + stepDim.steps, &ScreenStepDimSteps); + + Printf(true, " %Fl%Ll%fstep dimension now%E", &ScreenStepDimGo); + + Printf(true, "(or %Fl%Ll%fcancel operation%E)", &ScreenHome); +} + +//----------------------------------------------------------------------------- +// When we're creating tangent arcs (as requests, not as some parametric +// thing). User gets to specify the radius, and whether the old untrimmed +// curves are kept or deleted. +//----------------------------------------------------------------------------- +void TextWindow::ScreenChangeTangentArc(int link, uint32_t v) { + switch(link) { + case 'r': { + SS.TW.edit.meaning = Edit::TANGENT_ARC_RADIUS; + SS.TW.ShowEditControl(3, SS.MmToString(SS.tangentArcRadius, true)); + break; + } + + case 'a': SS.tangentArcManual = !SS.tangentArcManual; break; + case 'm': SS.tangentArcModify = !SS.tangentArcModify; break; + } +} +void TextWindow::ShowTangentArc() { + Printf(true, "%FtTANGENT ARC PARAMETERS%E"); + + Printf(true, "%Ft radius of created arc%E"); + if(SS.tangentArcManual) { + Printf(false, "%Ba %s %Fl%Lr%f[change]%E", + SS.MmToString(SS.tangentArcRadius).c_str(), + &(TextWindow::ScreenChangeTangentArc)); + } else { + Printf(false, "%Ba automatic"); + } + + Printf(false, ""); + Printf(false, " %Fd%f%La%s choose radius automatically%E", + &ScreenChangeTangentArc, + !SS.tangentArcManual ? CHECK_TRUE : CHECK_FALSE); + Printf(false, " %Fd%f%Lm%s modify original entities%E", + &ScreenChangeTangentArc, + SS.tangentArcModify ? CHECK_TRUE : CHECK_FALSE); + + Printf(false, ""); + Printf(false, "To create a tangent arc at a point,"); + Printf(false, "select that point and then choose"); + Printf(false, "Sketch -> Tangent Arc at Point."); + Printf(true, "(or %Fl%Ll%fback to home screen%E)", &ScreenHome); +} + +//----------------------------------------------------------------------------- +// The edit control is visible, and the user just pressed enter. +//----------------------------------------------------------------------------- +void TextWindow::EditControlDone(std::string s) { + edit.showAgain = false; + + switch(edit.meaning) { + case Edit::TIMES_REPEATED: + if(Expr *e = Expr::From(s, /*popUpError=*/true)) { + SS.UndoRemember(); + + double ev = e->Eval(); + if((int)ev < 1) { + Error(_("Can't repeat fewer than 1 time.")); + break; + } + if((int)ev > 999) { + Error(_("Can't repeat more than 999 times.")); + break; + } + + Group *g = SK.GetGroup(edit.group); + g->valA = ev; + + if(g->type == Group::Type::ROTATE) { + // If the group does not contain any constraints, then + // set the numerical guess to space the copies uniformly + // over one rotation. Don't touch the guess if we're + // already constrained, because that would break + // convergence. + if(g->GetNumConstraints() == 0) { + double copies = (g->skipFirst) ? (ev + 1) : ev; + SK.GetParam(g->h.param(3))->val = PI/(2*copies); + } + } + + SS.MarkGroupDirty(g->h); + } + break; + + case Edit::GROUP_NAME: + if(s.empty()) { + Error(_("Group name cannot be empty")); + } else { + SS.UndoRemember(); + + Group *g = SK.GetGroup(edit.group); + g->name = s; + } + break; + + case Edit::GROUP_SCALE: + if(Expr *e = Expr::From(s, /*popUpError=*/true)) { + double ev = e->Eval(); + if(fabs(ev) < 1e-6) { + Error(_("Scale cannot be zero.")); + } else { + Group *g = SK.GetGroup(edit.group); + g->scale = ev; + SS.MarkGroupDirty(g->h); + } + } + break; + + case Edit::HELIX_PITCH: // stored in valB + if(Expr *e = Expr::From(s, /*popUpError=*/true)) { + double ev = e->Eval(); + Group *g = SK.GetGroup(edit.group); + g->valB = ev * SS.MmPerUnit(); + SS.MarkGroupDirty(g->h); + } + break; + + case Edit::GROUP_COLOR: { + Vector rgb; + if(sscanf(s.c_str(), "%lf, %lf, %lf", &rgb.x, &rgb.y, &rgb.z)==3) { + rgb = rgb.ClampWithin(0, 1); + + Group *g = SK.group.FindByIdNoOops(SS.TW.shown.group); + if(!g) break; + g->color = RgbaColor::FromFloat((float)rgb.x, (float)rgb.y, (float)rgb.z, + g->color.alphaF()); + + SS.MarkGroupDirty(g->h); + SS.GW.ClearSuper(); + } else { + Error(_("Bad format: specify color as r, g, b")); + } + break; + } + case Edit::GROUP_OPACITY: + if(Expr *e = Expr::From(s, /*popUpError=*/true)) { + double alpha = e->Eval(); + if(alpha < 0 || alpha > 1) { + Error(_("Opacity must be between zero and one.")); + } else { + Group *g = SK.GetGroup(edit.group); + g->color.alpha = (int)(255.1f * alpha); + SS.MarkGroupDirty(g->h); + SS.GW.ClearSuper(); + } + } + break; + + case Edit::TTF_TEXT: + SS.UndoRemember(); + if(Request *r = SK.request.FindByIdNoOops(edit.request)) { + r->str = s; + SS.MarkGroupDirty(r->group); + } + break; + + case Edit::STEP_DIM_FINISH: + if(Expr *e = Expr::From(s, /*popUpError=*/true)) { + if(stepDim.isDistance) { + stepDim.finish = SS.ExprToMm(e); + } else { + stepDim.finish = e->Eval(); + } + } + break; + + case Edit::STEP_DIM_STEPS: + stepDim.steps = min(300, max(1, atoi(s.c_str()))); + break; + + case Edit::TANGENT_ARC_RADIUS: + if(Expr *e = Expr::From(s, /*popUpError=*/true)) { + if(e->Eval() < LENGTH_EPS) { + Error(_("Radius cannot be zero or negative.")); + break; + } + SS.tangentArcRadius = SS.ExprToMm(e); + } + break; + + default: { + int cnt = 0; + if(EditControlDoneForStyles(s)) cnt++; + if(EditControlDoneForConfiguration(s)) cnt++; + if(EditControlDoneForPaste(s)) cnt++; + if(EditControlDoneForView(s)) cnt++; + ssassert(cnt == 1, "Expected exactly one parameter to be edited"); + break; + } + } + SS.GW.Invalidate(); + SS.ScheduleShowTW(); + + if(!edit.showAgain) { + HideEditControl(); + edit.meaning = Edit::NOTHING; + } +} + +} // namespace SolveSpace diff --git a/src/textwin.cpp b/src/textwin.cpp new file mode 100644 index 0000000..537c2d1 --- /dev/null +++ b/src/textwin.cpp @@ -0,0 +1,1173 @@ +//----------------------------------------------------------------------------- +// Helper functions for the text-based browser window. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include +#include +#include "solvespace.h" + +namespace SolveSpace { + +class Button { +public: + virtual std::string Tooltip() = 0; + virtual void Draw(UiCanvas *uiCanvas, int x, int y, bool asHovered) = 0; + virtual int AdvanceWidth() = 0; + virtual void Click() = 0; +}; + +class SpacerButton : public Button { +public: + std::string Tooltip() override { return ""; } + + void Draw(UiCanvas *uiCanvas, int x, int y, bool asHovered) override { + // Draw a darker-grey spacer in between the groups of icons. + uiCanvas->DrawRect(x, x + 4, y, y - 24, + /*fillColor=*/{ 45, 45, 45, 255 }, + /*outlineColor=*/{}); + } + + int AdvanceWidth() override { return 12; } + + void Click() override {} +}; + +class ShowHideButton : public Button { +public: + bool *variable; + std::string tooltip; + std::string iconName; + std::shared_ptr icon; + + ShowHideButton(bool *variable, std::string iconName, std::string tooltip) + : variable(variable), tooltip(tooltip), iconName(iconName) {} + + std::string Tooltip() override { + return ((*variable) ? "Hide " : "Show ") + tooltip; + } + + void Draw(UiCanvas *uiCanvas, int x, int y, bool asHovered) override { + if(icon == NULL) { + icon = LoadPng("icons/text-window/" + iconName + ".png"); + } + + uiCanvas->DrawPixmap(icon, x, y - 24); + if(asHovered) { + uiCanvas->DrawRect(x - 2, x + 26, y + 2, y - 26, + /*fillColor=*/{ 255, 255, 0, 75 }, + /*outlineColor=*/{}); + } + if(!*(variable)) { + int s = 0, f = 23; + RgbaColor color = { 255, 0, 0, 150 }; + uiCanvas->DrawLine(x+s, y-s, x+f, y-f, color, 2); + uiCanvas->DrawLine(x+s, y-f, x+f, y-s, color, 2); + } + } + + int AdvanceWidth() override { return 32; } + + void Click() override { SS.GW.ToggleBool(variable); } +}; + +class FacesButton : public ShowHideButton { +public: + FacesButton() + : ShowHideButton(&(SS.GW.showFaces), "faces", "") {} + + std::string Tooltip() override { + if(*variable) { + return "Don't make faces selectable with mouse"; + } else { + return "Make faces selectable with mouse"; + } + } +}; + +#include +class TriStateButton : public Button { +public: + static const size_t tri = 3; + + TriStateButton(unsigned *variable, const std::array &states, + const std::array &tooltips, + const std::array &iconNames) + : variable(variable), states(states), tooltips(tooltips), iconNames(iconNames) { + } + + unsigned *const variable; + const std::array states; + const std::array tooltips; + const std::array iconNames; + std::shared_ptr icons[tri]; + + std::string Tooltip() override { + for(size_t k = 0; k < tri; ++k) + if(*variable == states[k]) + return tooltips[k]; + ssassert(false, "Unexpected mode"); + } + + void Draw(UiCanvas *uiCanvas, int x, int y, bool asHovered) override { + for(size_t k = 0; k < tri; ++k) + if(icons[k] == nullptr) + icons[k] = LoadPng("icons/text-window/" + iconNames[k] + ".png"); + + std::shared_ptr icon; + for(size_t k = 0; k < tri; ++k) + if(*variable == states[k]) { + icon = icons[k]; + break; + } + + uiCanvas->DrawPixmap(icon, x, y - 24); + if(asHovered) { + uiCanvas->DrawRect(x - 2, x + 26, y + 2, y - 26, + /*fillColor=*/{255, 255, 0, 75}, + /*outlineColor=*/{}); + } + } + + + int AdvanceWidth() override { return 32; } + + void Click() override { + for(size_t k = 0; k < tri; ++k) + if(*variable == states[k]) { + *variable = states[(k + 1) % tri]; + break; + } + + SS.GenerateAll(); + SS.GW.Invalidate(); + SS.ScheduleShowTW(); + } +}; + +static SpacerButton spacerButton; + +static ShowHideButton workplanesButton = + { &(SS.GW.showWorkplanes), "workplane", "workplanes from inactive groups" }; +static ShowHideButton normalsButton = + { &(SS.GW.showNormals), "normal", "normals" }; +static ShowHideButton pointsButton = + { &(SS.GW.showPoints), "point", "points" }; +static ShowHideButton constructionButton = + { &(SS.GW.showConstruction), "construction", "construction entities" }; +static TriStateButton constraintsButton = { + (unsigned *)(&(SS.GW.showConstraints)), + {(unsigned)GraphicsWindow::ShowConstraintMode::SCM_SHOW_ALL, + (unsigned)GraphicsWindow::ShowConstraintMode::SCM_SHOW_DIM, + (unsigned)GraphicsWindow::ShowConstraintMode::SCM_NOSHOW}, + {"Show only dimensions", "Hide constraints and dimensions", "Show constraints and dimensions"}, + {"constraint", "constraint-dimo", "constraint-wo"}}; +static FacesButton facesButton; +static ShowHideButton shadedButton = + { &(SS.GW.showShaded), "shaded", "shaded view of solid model" }; +static ShowHideButton edgesButton = + { &(SS.GW.showEdges), "edges", "edges of solid model" }; +static ShowHideButton outlinesButton = + { &(SS.GW.showOutlines), "outlines", "outline of solid model" }; +static ShowHideButton meshButton = + { &(SS.GW.showMesh), "mesh", "triangle mesh of solid model" }; +static TriStateButton occludedLinesButton = { + (unsigned *)(&(SS.GW.drawOccludedAs)), + {(unsigned)GraphicsWindow::DrawOccludedAs::INVISIBLE, + (unsigned)GraphicsWindow::DrawOccludedAs::STIPPLED, + (unsigned)GraphicsWindow::DrawOccludedAs::VISIBLE}, + {"Stipple occluded lines", "Draw occluded lines", "Don't draw occluded lines"}, + {"occluded-invisible", "occluded-stippled", "occluded-visible"}}; + +static Button *buttons[] = { + &workplanesButton, + &normalsButton, + &pointsButton, + &constructionButton, + &constraintsButton, + &facesButton, + &spacerButton, + &shadedButton, + &edgesButton, + &outlinesButton, + &meshButton, + &spacerButton, + &occludedLinesButton, +}; + +/** Foreground color codes. */ +const TextWindow::Color TextWindow::fgColors[] = { + { 'd', RGBi(255, 255, 255) }, // Default : white + { 'l', RGBi(100, 200, 255) }, // links : blue + { 't', RGBi(255, 200, 100) }, // tree/text : yellow + { 'h', RGBi( 90, 90, 90) }, + { 's', RGBi( 40, 255, 40) }, // Ok : green + { 'm', RGBi(200, 200, 0) }, + { 'r', RGBi( 0, 0, 0) }, // Reverse : black + { 'x', RGBi(255, 20, 20) }, // Error : red + { 'i', RGBi( 0, 255, 255) }, // Info : cyan + { 'g', RGBi(128, 128, 128) }, // Disabled : gray + { 'b', RGBi(200, 200, 200) }, + { 0, RGBi( 0, 0, 0) } +}; +/** Background color codes. */ +const TextWindow::Color TextWindow::bgColors[] = { + { 'd', RGBi( 0, 0, 0) }, // Default : black + { 't', RGBi( 34, 15, 15) }, + { 'a', RGBi( 25, 25, 25) }, // Alternate : dark gray + { 'r', RGBi(255, 255, 255) }, // Reverse : white + { 0, RGBi( 0, 0, 0) } +}; + +void TextWindow::MakeColorTable(const Color *in, float *out) { + int i; + for(i = 0; in[i].c != 0; i++) { + int c = in[i].c; + ssassert(c >= 0 && c <= 255, "Unexpected color index"); + out[c*3 + 0] = in[i].color.redF(); + out[c*3 + 1] = in[i].color.greenF(); + out[c*3 + 2] = in[i].color.blueF(); + } +} + +void TextWindow::Init() { + if(!window) { + window = Platform::CreateWindow(Platform::Window::Kind::TOOL, SS.GW.window); + if(window) { + canvas = CreateRenderer(); + + using namespace std::placeholders; + window->onClose = []() { + SS.TW.HideEditControl(); + SS.GW.showTextWindow = false; + SS.GW.EnsureValidActives(); + }; + window->onMouseEvent = [this](Platform::MouseEvent event) { + using Platform::MouseEvent; + + if(event.type == MouseEvent::Type::PRESS || + event.type == MouseEvent::Type::DBL_PRESS || + event.type == MouseEvent::Type::MOTION) { + bool isClick = (event.type != MouseEvent::Type::MOTION); + bool leftDown = (event.button == MouseEvent::Button::LEFT); + this->MouseEvent(isClick, leftDown, event.x, event.y); + return true; + } else if(event.type == MouseEvent::Type::LEAVE) { + MouseLeave(); + return true; + } else if(event.type == MouseEvent::Type::SCROLL_VERT) { + if (event.scrollDelta == 0) { + return true; + } + if (abs(event.scrollDelta) < 0.2) { + if (event.scrollDelta > 0) { + event.scrollDelta = 0.2; + } else { + event.scrollDelta = -0.2; + } + } + double offset = LINE_HEIGHT / 2 * event.scrollDelta; + ScrollbarEvent(window->GetScrollbarPosition() - offset); + } + return false; + }; + window->onKeyboardEvent = SS.GW.window->onKeyboardEvent; + window->onRender = std::bind(&TextWindow::Paint, this); + window->onEditingDone = std::bind(&TextWindow::EditControlDone, this, _1); + window->onScrollbarAdjusted = std::bind(&TextWindow::ScrollbarEvent, this, _1); + window->SetMinContentSize(370, 370); + } + } + + ClearSuper(); +} + +void TextWindow::ClearSuper() { + // Ugly hack, but not so ugly as the next line + Platform::WindowRef oldWindow = std::move(window); + std::shared_ptr oldCanvas = std::move(canvas); + + // Cannot use *this = {} here because TextWindow instances + // are 2.4MB long; this causes stack overflows in prologue + // when built with MSVC, even with optimizations. + this->~TextWindow(); + new(this) TextWindow(); + + // Return old canvas + window = std::move(oldWindow); + canvas = std::move(oldCanvas); + + HideEditControl(); + + MakeColorTable(fgColors, fgColorTable); + MakeColorTable(bgColors, bgColorTable); + + ClearScreen(); + Show(); +} + +void TextWindow::HideEditControl() { + editControl.colorPicker.show = false; + if(window) { + window->HideEditor(); + window->Invalidate(); + } +} + +void TextWindow::ShowEditControl(int col, const std::string &str, int halfRow) { + if(halfRow < 0) halfRow = top[hoveredRow]; + editControl.halfRow = halfRow; + editControl.col = col; + + int x = LEFT_MARGIN + CHAR_WIDTH_*col; + int y = (halfRow - SS.TW.scrollPos)*(LINE_HEIGHT/2); + + double width, height; + window->GetContentSize(&width, &height); + window->ShowEditor(x, y + LINE_HEIGHT - 2, LINE_HEIGHT - 4, + width - x, /*isMonospace=*/true, str); +} + +void TextWindow::ShowEditControlWithColorPicker(int col, RgbaColor rgb) { + SS.ScheduleShowTW(); + + editControl.colorPicker.show = true; + editControl.colorPicker.rgb = rgb; + editControl.colorPicker.h = 0; + editControl.colorPicker.s = 0; + editControl.colorPicker.v = 1; + ShowEditControl(col, ssprintf("%.2f, %.2f, %.2f", rgb.redF(), rgb.greenF(), rgb.blueF())); +} + +void TextWindow::ClearScreen() { + int i, j; + for(i = 0; i < MAX_ROWS; i++) { + for(j = 0; j < MAX_COLS; j++) { + text[i][j] = ' '; + meta[i][j].fg = 'd'; + meta[i][j].bg = 'd'; + meta[i][j].link = NOT_A_LINK; + } + top[i] = i*2; + } + rows = 0; +} + +// This message was added when someone had too many fonts for the text window +// Scrolling seemed to be broken, but was actually at the MAX_ROWS. +static const char* endString = " **** End of Text Screen ****"; + +void TextWindow::Printf(bool halfLine, const char *fmt, ...) { + if(!canvas) return; + + if(rows >= MAX_ROWS) return; + + if(rows >= MAX_ROWS-2 && (fmt != endString)) { + // twice due to some half-row issues on resizing + Printf(halfLine, endString); + Printf(halfLine, endString); + return; + } + + va_list vl; + va_start(vl, fmt); + + int r, c; + r = rows; + top[r] = (r == 0) ? 0 : (top[r-1] + (halfLine ? 3 : 2)); + rows++; + + for(c = 0; c < MAX_COLS; c++) { + text[r][c] = ' '; + meta[r][c].link = NOT_A_LINK; + } + + char fg = 'd'; + char bg = 'd'; + RgbaColor bgRgb = RGBi(0, 0, 0); + int link = NOT_A_LINK; + uint32_t data = 0; + LinkFunction *f = NULL, *h = NULL; + + c = 0; + while(*fmt) { + char buf[1024]; + + if(*fmt == '%') { + fmt++; + if(*fmt == '\0') goto done; + strcpy(buf, ""); + switch(*fmt) { + case 'd': { + int v = va_arg(vl, int); + sprintf(buf, "%d", v); + break; + } + case 'x': { + unsigned int v = va_arg(vl, unsigned int); + sprintf(buf, "%08x", v); + break; + } + case '@': { + double v = va_arg(vl, double); + sprintf(buf, "%.2f", v); + break; + } + case '2': { + double v = va_arg(vl, double); + sprintf(buf, "%s%.2f", v < 0 ? "" : " ", v); + break; + } + case '3': { + double v = va_arg(vl, double); + sprintf(buf, "%s%.3f", v < 0 ? "" : " ", v); + break; + } + case '#': { + double v = va_arg(vl, double); + sprintf(buf, "%.3f", v); + break; + } + case 's': { + char *s = va_arg(vl, char *); + memcpy(buf, s, min(sizeof(buf), strlen(s)+1)); + break; + } + case 'c': { + // 'char' is promoted to 'int' when passed through '...' + int v = va_arg(vl, int); + if(v == 0) { + strcpy(buf, ""); + } else { + sprintf(buf, "%c", v); + } + break; + } + case 'E': + fg = 'd'; + // leave the background, though + link = NOT_A_LINK; + data = 0; + f = NULL; + h = NULL; + break; + + case 'F': + case 'B': { + char cc = fmt[1]; // color code + RgbaColor *rgbPtr = NULL; + switch(cc) { + case 0: goto done; // truncated directive + case 'p': cc = (char)va_arg(vl, int); break; + case 'z': rgbPtr = va_arg(vl, RgbaColor *); break; + } + if(*fmt == 'F') { + fg = cc; + } else { + bg = cc; + if(rgbPtr) bgRgb = *rgbPtr; + } + fmt++; + break; + } + case 'L': + if(fmt[1] == '\0') goto done; + fmt++; + if(*fmt == 'p') { + link = va_arg(vl, int); + } else { + link = *fmt; + } + break; + + case 'f': + f = va_arg(vl, LinkFunction *); + break; + + case 'h': + h = va_arg(vl, LinkFunction *); + break; + + case 'D': { + unsigned int v = va_arg(vl, unsigned int); + data = (uint32_t)v; + break; + } + case '%': + strcpy(buf, "%"); + break; + } + } else { + utf8_iterator it2(fmt), it1 = it2++; + strncpy(buf, fmt, it2 - it1); + buf[it2 - it1] = '\0'; + } + + for(utf8_iterator it(buf); *it; ++it) { + for(size_t i = 0; i < canvas->GetBitmapFont()->GetWidth(*it); i++) { + if(c >= MAX_COLS) goto done; + text[r][c] = (i == 0) ? *it : ' '; + meta[r][c].fg = fg; + meta[r][c].bg = bg; + meta[r][c].bgRgb = bgRgb; + meta[r][c].link = link; + meta[r][c].data = data; + meta[r][c].f = f; + meta[r][c].h = h; + c++; + } + } + + utf8_iterator it(fmt); + it++; + fmt = it.ptr(); + } + while(c < MAX_COLS) { + meta[r][c].fg = fg; + meta[r][c].bg = bg; + meta[r][c].bgRgb = bgRgb; + c++; + } + +done: + va_end(vl); +} + +void TextWindow::Show() { + if(SS.GW.pending.operation == GraphicsWindow::Pending::NONE) { + SS.GW.ClearPending(/*scheduleShowTW=*/false); + } + + SS.GW.GroupSelection(); + auto const &gs = SS.GW.gs; + + // Make sure these tests agree with test used to draw indicator line on + // main list of groups screen. + if(SS.GW.pending.description) { + // A pending operation (that must be completed with the mouse in + // the graphics window) will preempt our usual display. + HideEditControl(); + ShowHeader(false); + Printf(false, ""); + Printf(false, "%s", SS.GW.pending.description); + Printf(true, "%Fl%f%Ll(cancel operation)%E", + &TextWindow::ScreenUnselectAll); + } else if((gs.n > 0 || gs.constraints > 0) && + shown.screen != Screen::PASTE_TRANSFORMED) + { + if(edit.meaning != Edit::TTF_TEXT) HideEditControl(); + ShowHeader(false); + DescribeSelection(); + } else { + if(edit.meaning == Edit::TTF_TEXT) HideEditControl(); + ShowHeader(true); + switch(shown.screen) { + default: + shown.screen = Screen::LIST_OF_GROUPS; + // fall through + case Screen::LIST_OF_GROUPS: ShowListOfGroups(); break; + case Screen::GROUP_INFO: ShowGroupInfo(); break; + case Screen::GROUP_SOLVE_INFO: ShowGroupSolveInfo(); break; + case Screen::CONFIGURATION: ShowConfiguration(); break; + case Screen::STEP_DIMENSION: ShowStepDimension(); break; + case Screen::LIST_OF_STYLES: ShowListOfStyles(); break; + case Screen::STYLE_INFO: ShowStyleInfo(); break; + case Screen::PASTE_TRANSFORMED: ShowPasteTransformed(); break; + case Screen::EDIT_VIEW: ShowEditView(); break; + case Screen::TANGENT_ARC: ShowTangentArc(); break; + } + } + Printf(false, ""); + + // Make sure there's room for the color picker + if(editControl.colorPicker.show) { + int pickerHeight = 25; + int halfRow = editControl.halfRow; + if(top[rows-1] - halfRow < pickerHeight && rows < MAX_ROWS) { + rows++; + top[rows-1] = halfRow + pickerHeight; + } + } + + if(window) Resize(); +} + +void TextWindow::Resize() +{ + double width, height; + window->GetContentSize(&width, &height); + + halfRows = (int)height / (LINE_HEIGHT/2); + + int bottom = top[rows-1] + 2; + scrollPos = min(scrollPos, bottom - halfRows); + scrollPos = max(scrollPos, 0); + + window->ConfigureScrollbar(0, top[rows - 1] + 1, halfRows); + window->SetScrollbarPosition(scrollPos); + window->SetScrollbarVisible(top[rows - 1] + 1 > halfRows); + window->Invalidate(); +} + +void TextWindow::DrawOrHitTestIcons(UiCanvas *uiCanvas, TextWindow::DrawOrHitHow how, + double mx, double my) +{ + double width, height; + window->GetContentSize(&width, &height); + + int x = 20, y = 33 + LINE_HEIGHT; + y -= scrollPos*(LINE_HEIGHT/2); + + if(how == PAINT) { + int top = y - 28, bot = y + 4; + uiCanvas->DrawRect(0, (int)width, top, bot, + /*fillColor=*/{ 30, 30, 30, 255 }, /*outlineColor=*/{}); + } + + Button *oldHovered = hoveredButton; + if(how != PAINT) { + hoveredButton = NULL; + } + + double hoveredX = 0, hoveredY = 0; + for(Button *button : buttons) { + if(how == PAINT) { + button->Draw(uiCanvas, x, y, (button == hoveredButton)); + } else if(mx > x - 2 && mx < x + 26 && + my < y + 2 && my > y - 26) { + hoveredButton = button; + hoveredX = x - 2; + hoveredY = y - 26; + if(how == CLICK) { + button->Click(); + } + } + + x += button->AdvanceWidth(); + } + + if(how != PAINT && hoveredButton != oldHovered) { + if(hoveredButton == NULL) { + window->SetTooltip("", 0, 0, 0, 0); + } else { + window->SetTooltip(hoveredButton->Tooltip(), hoveredX, hoveredY, 28, 28); + } + window->Invalidate(); + } +} + +//---------------------------------------------------------------------------- +// Given (x, y, z) = (h, s, v) in [0,6), [0,1], [0,1], return (x, y, z) = +// (r, g, b) all in [0, 1]. +//---------------------------------------------------------------------------- +Vector TextWindow::HsvToRgb(Vector hsv) { + if(hsv.x >= 6) hsv.x -= 6; + + Vector rgb; + double hmod2 = hsv.x; + while(hmod2 >= 2) hmod2 -= 2; + double x = (1 - fabs(hmod2 - 1)); + if(hsv.x < 1) { + rgb = Vector::From(1, x, 0); + } else if(hsv.x < 2) { + rgb = Vector::From(x, 1, 0); + } else if(hsv.x < 3) { + rgb = Vector::From(0, 1, x); + } else if(hsv.x < 4) { + rgb = Vector::From(0, x, 1); + } else if(hsv.x < 5) { + rgb = Vector::From(x, 0, 1); + } else { + rgb = Vector::From(1, 0, x); + } + double c = hsv.y*hsv.z; + double m = 1 - hsv.z; + rgb = rgb.ScaledBy(c); + rgb = rgb.Plus(Vector::From(m, m, m)); + + return rgb; +} + +std::shared_ptr TextWindow::HsvPattern2d(int w, int h) { + std::shared_ptr pixmap = Pixmap::Create(Pixmap::Format::RGB, w, h); + for(size_t j = 0; j < pixmap->height; j++) { + size_t p = pixmap->stride * j; + for(size_t i = 0; i < pixmap->width; i++) { + Vector hsv = Vector::From(6.0*i/(pixmap->width-1), 1.0*j/(pixmap->height-1), 1); + Vector rgb = HsvToRgb(hsv); + rgb = rgb.ScaledBy(255); + pixmap->data[p++] = (uint8_t)rgb.x; + pixmap->data[p++] = (uint8_t)rgb.y; + pixmap->data[p++] = (uint8_t)rgb.z; + } + } + return pixmap; +} + +std::shared_ptr TextWindow::HsvPattern1d(double hue, double sat, int w, int h) { + std::shared_ptr pixmap = Pixmap::Create(Pixmap::Format::RGB, w, h); + for(size_t i = 0; i < pixmap->height; i++) { + size_t p = i * pixmap->stride; + for(size_t j = 0; j < pixmap->width; j++) { + Vector hsv = Vector::From(6*hue, sat, 1.0*(pixmap->width - 1 - j)/pixmap->width); + Vector rgb = HsvToRgb(hsv); + rgb = rgb.ScaledBy(255); + pixmap->data[p++] = (uint8_t)rgb.x; + pixmap->data[p++] = (uint8_t)rgb.y; + pixmap->data[p++] = (uint8_t)rgb.z; + } + } + return pixmap; +} + +void TextWindow::ColorPickerDone() { + RgbaColor rgb = editControl.colorPicker.rgb; + EditControlDone(ssprintf("%.2f, %.2f, %.3f", rgb.redF(), rgb.greenF(), rgb.blueF())); +} + +bool TextWindow::DrawOrHitTestColorPicker(UiCanvas *uiCanvas, DrawOrHitHow how, bool leftDown, + double x, double y) +{ + using Platform::Window; + + bool mousePointerAsHand = false; + + if(how == HOVER && !leftDown) { + editControl.colorPicker.picker1dActive = false; + editControl.colorPicker.picker2dActive = false; + } + + if(!editControl.colorPicker.show) return false; + if(how == CLICK || (how == HOVER && leftDown)) window->Invalidate(); + + static const RgbaColor BaseColor[12] = { + RGBi(255, 0, 0), + RGBi( 0, 255, 0), + RGBi( 0, 0, 255), + + RGBi( 0, 255, 255), + RGBi(255, 0, 255), + RGBi(255, 255, 0), + + RGBi(255, 127, 0), + RGBi(255, 0, 127), + RGBi( 0, 255, 127), + RGBi(127, 255, 0), + RGBi(127, 0, 255), + RGBi( 0, 127, 255), + }; + + double width, height; + window->GetContentSize(&width, &height); + + int px = LEFT_MARGIN + CHAR_WIDTH_*editControl.col; + int py = (editControl.halfRow - SS.TW.scrollPos)*(LINE_HEIGHT/2); + + py += LINE_HEIGHT + 5; + + static const int WIDTH = 16, HEIGHT = 12; + static const int PITCH = 18, SIZE = 15; + + px = min(px, (int)width - (WIDTH*PITCH + 40)); + + int pxm = px + WIDTH*PITCH + 11, + pym = py + HEIGHT*PITCH + 7; + + int bw = 6; + if(how == PAINT) { + uiCanvas->DrawRect(px, pxm+bw, py, pym+bw, + /*fillColor=*/{ 50, 50, 50, 255 }, + /*outlineColor=*/{}, + /*zIndex=*/1); + uiCanvas->DrawRect(px+(bw/2), pxm+(bw/2), py+(bw/2), pym+(bw/2), + /*fillColor=*/{ 0, 0, 0, 255 }, + /*outlineColor=*/{}, + /*zIndex=*/1); + } else { + if(x < px || x > pxm+(bw/2) || + y < py || y > pym+(bw/2)) + { + return false; + } + } + px += (bw/2); + py += (bw/2); + + int i, j; + for(i = 0; i < WIDTH/2; i++) { + for(j = 0; j < HEIGHT; j++) { + Vector rgb; + RgbaColor d; + if(i == 0 && j < 8) { + d = SS.modelColor[j]; + rgb = Vector::From(d.redF(), d.greenF(), d.blueF()); + } else if(i == 0) { + double a = (j - 8.0)/3.0; + rgb = Vector::From(a, a, a); + } else { + d = BaseColor[j]; + rgb = Vector::From(d.redF(), d.greenF(), d.blueF()); + if(i >= 2 && i <= 4) { + double a = (i == 2) ? 0.2 : (i == 3) ? 0.3 : 0.4; + rgb = rgb.Plus(Vector::From(a, a, a)); + } + if(i >= 5 && i <= 7) { + double a = (i == 5) ? 0.7 : (i == 6) ? 0.4 : 0.18; + rgb = rgb.ScaledBy(a); + } + } + + rgb = rgb.ClampWithin(0, 1); + int sx = px + 5 + PITCH*(i + 8) + 4, sy = py + 5 + PITCH*j; + + if(how == PAINT) { + uiCanvas->DrawRect(sx, sx+SIZE, sy, sy+SIZE, + /*fillColor=*/RGBf(rgb.x, rgb.y, rgb.z), + /*outlineColor=*/{}, + /*zIndex=*/2); + } else if(how == CLICK) { + if(x >= sx && x <= sx+SIZE && y >= sy && y <= sy+SIZE) { + editControl.colorPicker.rgb = RGBf(rgb.x, rgb.y, rgb.z); + ColorPickerDone(); + } + } else if(how == HOVER) { + if(x >= sx && x <= sx+SIZE && y >= sy && y <= sy+SIZE) { + mousePointerAsHand = true; + } + } + } + } + + int hxm, hym; + int hx = px + 5, hy = py + 5; + hxm = hx + PITCH*7 + SIZE; + hym = hy + PITCH*2 + SIZE; + if(how == PAINT) { + uiCanvas->DrawRect(hx, hxm, hy, hym, + /*fillColor=*/editControl.colorPicker.rgb, + /*outlineColor=*/{}, + /*zIndex=*/2); + } else if(how == CLICK) { + if(x >= hx && x <= hxm && y >= hy && y <= hym) { + ColorPickerDone(); + } + } else if(how == HOVER) { + if(x >= hx && x <= hxm && y >= hy && y <= hym) { + mousePointerAsHand = true; + } + } + + hy += PITCH*3; + + hxm = hx + PITCH*7 + SIZE; + hym = hy + PITCH*1 + SIZE; + // The one-dimensional thing to pick the color's value + if(how == PAINT) { + uiCanvas->DrawPixmap(HsvPattern1d(editControl.colorPicker.h, + editControl.colorPicker.s, + hxm-hx, hym-hy), + hx, hy, /*zIndex=*/2); + + int cx = hx+(int)((hxm-hx)*(1.0 - editControl.colorPicker.v)); + uiCanvas->DrawLine(cx, hy, cx, hym, + /*fillColor=*/{ 0, 0, 0, 255 }, + /*outlineColor=*/{}, + /*zIndex=*/3); + } else if(how == CLICK || + (how == HOVER && leftDown && editControl.colorPicker.picker1dActive)) + { + if(x >= hx && x <= hxm && y >= hy && y <= hym) { + editControl.colorPicker.v = 1 - (x - hx)/(hxm - hx); + + Vector rgb = HsvToRgb(Vector::From( + 6*editControl.colorPicker.h, + editControl.colorPicker.s, + editControl.colorPicker.v)); + editControl.colorPicker.rgb = RGBf(rgb.x, rgb.y, rgb.z); + + editControl.colorPicker.picker1dActive = true; + } + } + // and advance our vertical position + hy += PITCH*2; + + hxm = hx + PITCH*7 + SIZE; + hym = hy + PITCH*6 + SIZE; + // Two-dimensional thing to pick a color by hue and saturation + if(how == PAINT) { + uiCanvas->DrawPixmap(HsvPattern2d(hxm-hx, hym-hy), hx, hy, + /*zIndex=*/2); + + int cx = hx+(int)((hxm-hx)*editControl.colorPicker.h), + cy = hy+(int)((hym-hy)*editControl.colorPicker.s); + uiCanvas->DrawLine(cx - 5, cy, cx + 5, cy, + /*fillColor=*/{ 255, 255, 255, 255 }, + /*outlineColor=*/{}, + /*zIndex=*/3); + uiCanvas->DrawLine(cx, cy - 5, cx, cy + 5, + /*fillColor=*/{ 255, 255, 255, 255 }, + /*outlineColor=*/{}, + /*zIndex=*/3); + } else if(how == CLICK || + (how == HOVER && leftDown && editControl.colorPicker.picker2dActive)) + { + if(x >= hx && x <= hxm && y >= hy && y <= hym) { + double h = (x - hx)/(hxm - hx), + s = (y - hy)/(hym - hy); + editControl.colorPicker.h = h; + editControl.colorPicker.s = s; + + Vector rgb = HsvToRgb(Vector::From( + 6*editControl.colorPicker.h, + editControl.colorPicker.s, + editControl.colorPicker.v)); + editControl.colorPicker.rgb = RGBf(rgb.x, rgb.y, rgb.z); + + editControl.colorPicker.picker2dActive = true; + } + } + + window->SetCursor(mousePointerAsHand ? + Window::Cursor::HAND : + Window::Cursor::POINTER); + return true; +} + +void TextWindow::Paint() { + if (!canvas) return; + + double width, height; + window->GetContentSize(&width, &height); + if(halfRows != (int)height / (LINE_HEIGHT/2)) + Resize(); + + Camera camera = {}; + camera.width = width; + camera.height = height; + camera.pixelRatio = window->GetDevicePixelRatio(); + camera.gridFit = (window->GetDevicePixelRatio() == 1); + camera.LoadIdentity(); + camera.offset.x = -camera.width / 2.0; + camera.offset.y = -camera.height / 2.0; + + Lighting lighting = {}; + lighting.backgroundColor = RGBi(0, 0, 0); + + canvas->SetLighting(lighting); + canvas->SetCamera(camera); + canvas->StartFrame(); + + UiCanvas uiCanvas = {}; + uiCanvas.canvas = canvas; + uiCanvas.flip = true; + + int r, c, a; + for(a = 0; a < 2; a++) { + for(r = 0; r < rows; r++) { + int ltop = top[r]; + if(ltop < (scrollPos-1)) continue; + if(ltop > scrollPos+halfRows) break; + + for(c = 0; c < min(((int)width/CHAR_WIDTH_)+1, (int) MAX_COLS); c++) { + int x = LEFT_MARGIN + c*CHAR_WIDTH_; + int y = (ltop-scrollPos)*(LINE_HEIGHT/2) + 4; + + int fg = meta[r][c].fg; + int bg = meta[r][c].bg; + + // On the first pass, all the background quads; on the next + // pass, all the foreground (i.e., font) quads. + if(a == 0) { + RgbaColor bgRgb = meta[r][c].bgRgb; + int bh = LINE_HEIGHT, adj = 0; + if(bg == 'z') { + bh = CHAR_HEIGHT; + adj += 2; + } else { + bgRgb = RgbaColor::FromFloat(bgColorTable[bg*3+0], + bgColorTable[bg*3+1], + bgColorTable[bg*3+2]); + } + + if(bg != 'd') { + // Move the quad down a bit, so that the descenders + // still have the correct background. + uiCanvas.DrawRect(x, x + CHAR_WIDTH_, y + adj, y + adj + bh, + /*fillColor=*/bgRgb, /*outlineColor=*/{}); + } + } else if(a == 1) { + RgbaColor fgRgb = RgbaColor::FromFloat(fgColorTable[fg*3+0], + fgColorTable[fg*3+1], + fgColorTable[fg*3+2]); + if(text[r][c] != ' ') { + uiCanvas.DrawBitmapChar(text[r][c], x, y + CHAR_HEIGHT, fgRgb); + } + + // If this is a link and it's hovered, then draw the + // underline + if(meta[r][c].link && meta[r][c].link != 'n' && + (r == hoveredRow && c == hoveredCol)) + { + int cs = c, cf = c; + while(cs >= 0 && meta[r][cs].link && + meta[r][cs].f == meta[r][c].f && + meta[r][cs].data == meta[r][c].data) + { + cs--; + } + cs++; + + while( meta[r][cf].link && + meta[r][cf].f == meta[r][c].f && + meta[r][cf].data == meta[r][c].data) + { + cf++; + } + + // But don't underline checkboxes or radio buttons + while(((text[r][cs] >= 0xe000 && text[r][cs] <= 0xefff) || + text[r][cs] == ' ') && + cs < cf) + { + cs++; + } + + // Always use the color of the rightmost character + // in the link, so that underline is consistent color + fg = meta[r][cf-1].fg; + fgRgb = RgbaColor::FromFloat(fgColorTable[fg*3+0], + fgColorTable[fg*3+1], + fgColorTable[fg*3+2]); + int yp = y + CHAR_HEIGHT; + uiCanvas.DrawLine(LEFT_MARGIN + cs*CHAR_WIDTH_, yp, + LEFT_MARGIN + cf*CHAR_WIDTH_, yp, + fgRgb); + } + } + } + } + } + + // The line to indicate the column of radio buttons that indicates the + // active group. + SS.GW.GroupSelection(); + auto const &gs = SS.GW.gs; + // Make sure this test agrees with test to determine which screen is drawn + if(!SS.GW.pending.description && gs.n == 0 && gs.constraints == 0 && + shown.screen == Screen::LIST_OF_GROUPS) + { + int x = 29, y = 70 + LINE_HEIGHT; + y -= scrollPos*(LINE_HEIGHT/2); + + RgbaColor color = RgbaColor::FromFloat(fgColorTable['t'*3+0], + fgColorTable['t'*3+1], + fgColorTable['t'*3+2]); + uiCanvas.DrawLine(x, y, x, y+40, color); + } + + // The header has some icons that are drawn separately from the text + DrawOrHitTestIcons(&uiCanvas, PAINT, 0, 0); + + // And we may show a color picker for certain editable fields + DrawOrHitTestColorPicker(&uiCanvas, PAINT, false, 0, 0); + + canvas->FlushFrame(); + canvas->FinishFrame(); + canvas->Clear(); +} + +void TextWindow::MouseEvent(bool leftClick, bool leftDown, double x, double y) { + using Platform::Window; + + if(SS.TW.window->IsEditorVisible() || SS.GW.window->IsEditorVisible()) { + if(DrawOrHitTestColorPicker(NULL, leftClick ? CLICK : HOVER, leftDown, x, y)) { + return; + } + + if(leftClick) { + HideEditControl(); + SS.GW.window->HideEditor(); + } else { + window->SetCursor(Window::Cursor::POINTER); + } + return; + } + + DrawOrHitTestIcons(NULL, leftClick ? CLICK : HOVER, x, y); + + GraphicsWindow::Selection ps = SS.GW.hover; + SS.GW.hover.Clear(); + + int prevHoveredRow = hoveredRow, + prevHoveredCol = hoveredCol; + hoveredRow = 0; + hoveredCol = 0; + + // Find the corresponding character in the text buffer + int c = (int)((x - LEFT_MARGIN) / CHAR_WIDTH_); + int hh = (LINE_HEIGHT)/2; + y += scrollPos*hh; + int r; + for(r = 0; r < rows; r++) { + if(y >= top[r]*hh && y <= (top[r]+2)*hh) { + break; + } + } + if(r >= 0 && c >= 0 && r < rows && c < MAX_COLS) { + window->SetCursor(Window::Cursor::POINTER); + + hoveredRow = r; + hoveredCol = c; + + const auto &item = meta[r][c]; + if(leftClick) { + if(item.link && item.f) { + (item.f)(item.link, item.data); + Show(); + SS.GW.Invalidate(); + } + } else { + if(item.link) { + window->SetCursor(Window::Cursor::HAND); + if(item.h) { + (item.h)(item.link, item.data); + } + } else { + window->SetCursor(Window::Cursor::POINTER); + } + } + } + + if((!ps.Equals(&(SS.GW.hover))) || + prevHoveredRow != hoveredRow || + prevHoveredCol != hoveredCol) + { + SS.GW.Invalidate(); + window->Invalidate(); + } +} + +void TextWindow::MouseLeave() { + hoveredButton = NULL; + hoveredRow = 0; + hoveredCol = 0; + window->Invalidate(); +} + +void TextWindow::ScrollbarEvent(double newPos) { + if(window->IsEditorVisible()) { + // An edit field is active. Do not move the scrollbar. + return; + } + + int bottom = top[rows-1] + 2; + newPos = min((int)newPos, bottom - halfRows); + newPos = max((int)newPos, 0); + if(newPos != scrollPos) { + scrollPos = (int)newPos; + window->SetScrollbarPosition(scrollPos); + window->Invalidate(); + } +} + +} diff --git a/src/toolbar.cpp b/src/toolbar.cpp new file mode 100644 index 0000000..372b909 --- /dev/null +++ b/src/toolbar.cpp @@ -0,0 +1,252 @@ +//----------------------------------------------------------------------------- +// The toolbar that appears at the top left of the graphics window, where the +// user can select icons with the mouse, to perform operations equivalent to +// selecting a menu item or using a keyboard shortcut. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +struct ToolIcon { + std::string name; + Command command; + const char *tooltip; + std::shared_ptr pixmap; +}; +static ToolIcon Toolbar[] = { + { "line", Command::LINE_SEGMENT, + N_("Sketch line segment"), {} }, + { "rectangle", Command::RECTANGLE, + N_("Sketch rectangle"), {} }, + { "circle", Command::CIRCLE, + N_("Sketch circle"), {} }, + { "arc", Command::ARC, + N_("Sketch arc of a circle"), {} }, + { "text", Command::TTF_TEXT, + N_("Sketch curves from text in a TrueType font"), {} }, + { "image", Command::IMAGE, + N_("Sketch image from a file"), {} }, + { "tangent-arc", Command::TANGENT_ARC, + N_("Create tangent arc at selected point"), {} }, + { "bezier", Command::CUBIC, + N_("Sketch cubic Bezier spline"), {} }, + { "point", Command::DATUM_POINT, + N_("Sketch datum point"), {} }, + { "construction", Command::CONSTRUCTION, + N_("Toggle construction"), {} }, + { "trim", Command::SPLIT_CURVES, + N_("Split lines / curves where they intersect"), {} }, + { "", Command::NONE, "", {} }, + + { "length", Command::DISTANCE_DIA, + N_("Constrain distance / diameter / length"), {} }, + { "angle", Command::ANGLE, + N_("Constrain angle"), {} }, + { "horiz", Command::HORIZONTAL, + N_("Constrain to be horizontal"), {} }, + { "vert", Command::VERTICAL, + N_("Constrain to be vertical"), {} }, + { "parallel", Command::PARALLEL, + N_("Constrain to be parallel or tangent"), {} }, + { "perpendicular", Command::PERPENDICULAR, + N_("Constrain to be perpendicular"), {} }, + { "pointonx", Command::ON_ENTITY, + N_("Constrain point on line / curve / plane / point"), {} }, + { "symmetric", Command::SYMMETRIC, + N_("Constrain symmetric"), {} }, + { "equal", Command::EQUAL, + N_("Constrain equal length / radius / angle"), {} }, + { "same-orientation",Command::ORIENTED_SAME, + N_("Constrain normals in same orientation"), {} }, + { "other-supp", Command::OTHER_ANGLE, + N_("Other supplementary angle"), {} }, + { "ref", Command::REFERENCE, + N_("Toggle reference dimension"), {} }, + { "", Command::NONE, "", {} }, + + { "extrude", Command::GROUP_EXTRUDE, + N_("New group extruding active sketch"), {} }, + { "lathe", Command::GROUP_LATHE, + N_("New group rotating active sketch"), {} }, + { "helix", Command::GROUP_HELIX, + N_("New group helix from active sketch"), {} }, + { "revolve", Command::GROUP_REVOLVE, + N_("New group revolve active sketch"), {} }, + { "step-rotate", Command::GROUP_ROT, + N_("New group step and repeat rotating"), {} }, + { "step-translate", Command::GROUP_TRANS, + N_("New group step and repeat translating"), {} }, + { "sketch-in-plane", Command::GROUP_WRKPL, + N_("New group in new workplane (thru given entities)"), {} }, + { "sketch-in-3d", Command::GROUP_3D, + N_("New group in 3d"), {} }, + { "assemble", Command::GROUP_LINK, + N_("New group linking / assembling file"), {} }, + { "", Command::NONE, "", {} }, + + { "in3d", Command::NEAREST_ISO, + N_("Nearest isometric view"), {} }, + { "ontoworkplane", Command::ONTO_WORKPLANE, + N_("Align view to active workplane"), {} }, +}; + +void GraphicsWindow::ToolbarDraw(UiCanvas *canvas) { + ToolbarDrawOrHitTest(0, 0, canvas, NULL, NULL, NULL); +} + +bool GraphicsWindow::ToolbarMouseMoved(int x, int y) { + double width, height; + window->GetContentSize(&width, &height); + + x += ((int)width/2); + y += ((int)height/2); + + Command hitCommand; + int hitX, hitY; + bool withinToolbar = ToolbarDrawOrHitTest(x, y, NULL, &hitCommand, &hitX, &hitY); + + if(hitCommand != toolbarHovered) { + toolbarHovered = hitCommand; + Invalidate(); + } + + if(toolbarHovered != Command::NONE) { + std::string tooltip; + for(ToolIcon &icon : Toolbar) { + if(toolbarHovered == icon.command) { + tooltip = Translate(icon.tooltip); + } + } + + Platform::KeyboardEvent accel = SS.GW.AcceleratorForCommand(toolbarHovered); + std::string accelDesc = Platform::AcceleratorDescription(accel); + if(!accelDesc.empty()) { + tooltip += ssprintf(" (%s)", accelDesc.c_str()); + } + + window->SetTooltip(tooltip, hitX, hitY, 32, 32); + } else { + window->SetTooltip("", 0, 0, 0, 0); + } + + return withinToolbar; +} + +bool GraphicsWindow::ToolbarMouseDown(int x, int y) { + double width, height; + window->GetContentSize(&width, &height); + + x += ((int)width/2); + y += ((int)height/2); + + Command hitCommand; + bool withinToolbar = ToolbarDrawOrHitTest(x, y, NULL, &hitCommand, NULL, NULL); + if(hitCommand != Command::NONE) { + SS.GW.ActivateCommand(hitCommand); + } + return withinToolbar; +} + +bool GraphicsWindow::ToolbarDrawOrHitTest(int mx, int my, UiCanvas *canvas, + Command *hitCommand, int *hitX, int *hitY) +{ + double width, height; + window->GetContentSize(&width, &height); + + int x = 17, y = (int)(height - 21); // 20 is the menu bar height + + // When changing these values, also change the asReference drawing code in drawentity.cpp + // as well as the "window->SetMinContentSize(720, 636);" in graphicswin.cpp + int fudge = 8; + int h = 32*18 + 3*16 + fudge; // Toolbar height = 18 icons * 32 pixels + 3 dividers * 16 pixels + fudge + + if(h < y) { + // If there is enough vertical space leave up to 32 pixels between the menu bar and the toolbar. + y -= ((y - h) < 32) ? y - h : 32; + } + + int aleft = 0, aright = 66, atop = y+16+fudge/2, abot = y+16-h; + + bool withinToolbar = + (mx >= aleft && mx <= aright && my <= atop && my >= abot); + + // Initialize/clear hitCommand. + if(hitCommand) *hitCommand = Command::NONE; + + if(!canvas && !withinToolbar) { + // This gets called every MouseMove event, so return quickly. + return false; + } + + if(canvas) { + canvas->DrawRect(aleft, aright, atop, abot, + /*fillColor=*/{ 30, 30, 30, 255 }, + /*outlineColor=*/{}); + } + + bool leftpos = true; + for(ToolIcon &icon : Toolbar) { + if(icon.name.empty()) { // spacer + if(!leftpos) { + leftpos = true; + y -= 32; + x -= 32; + } + y -= 16; + + if(canvas) { + // Draw a separator bar in a slightly different color. + int divw = 30, divh = 2; + canvas->DrawRect(x+16+divw, x+16-divw, y+24+divh, y+24-divh, + /*fillColor=*/{ 45, 45, 45, 255 }, + /*outlineColor=*/{}); + } + + continue; + } + + if(icon.pixmap == nullptr) { + icon.pixmap = LoadPng("icons/graphics-window/" + icon.name + ".png"); + } + + if(canvas) { + canvas->DrawPixmap(icon.pixmap, + x - (int)icon.pixmap->width / 2, + y - (int)icon.pixmap->height / 2); + + if(toolbarHovered == icon.command || + (pending.operation == Pending::COMMAND && + pending.command == icon.command)) { + // Highlight the hovered or pending item. + const int boxhw = 15; + canvas->DrawRect(x+boxhw, x-boxhw, y+boxhw, y-boxhw, + /*fillColor=*/{ 255, 255, 0, 75 }, + /*outlineColor=*/{}); + } + } else { + const int boxhw = 16; + if(mx < (x+boxhw) && mx > (x - boxhw) && + my < (y+boxhw) && my > (y - boxhw)) + { + if(hitCommand) *hitCommand = icon.command; + if(hitX) *hitX = x - boxhw; + if(hitY) *hitY = (int)height - (y + boxhw); + } + } + + if(leftpos) { + x += 32; + leftpos = false; + } else { + x -= 32; + y -= 32; + leftpos = true; + } + } + + return withinToolbar; +} + +} // namespace SolveSpace diff --git a/src/ttf.cpp b/src/ttf.cpp new file mode 100644 index 0000000..2aa928b --- /dev/null +++ b/src/ttf.cpp @@ -0,0 +1,449 @@ +//----------------------------------------------------------------------------- +// Routines to read a TrueType font as vector outlines, and generate them +// as entities, since they're always representable as either lines or +// quadratic Bezier curves. +// +// Copyright 2016 whitequark, Peter Barfuss. +//----------------------------------------------------------------------------- +#include +#include FT_FREETYPE_H +#include FT_OUTLINE_H +#include FT_ADVANCES_H + +/* Yecch. Irritatingly, you need to do this nonsense to get the error string table, + since nobody thought to put this exact function into FreeType itself. */ +#undef __FTERRORS_H__ +#define FT_ERRORDEF(e, v, s) { (e), (s) }, +#define FT_ERROR_START_LIST +#define FT_ERROR_END_LIST { 0, NULL } + +struct ft_error { + int err; + const char *str; +}; + +static const struct ft_error ft_errors[] = { +#include FT_ERRORS_H +}; + +extern "C" const char *ft_error_string(int err) { + const struct ft_error *e; + for(e = ft_errors; e->str; e++) + if(e->err == err) + return e->str; + return "Unknown error"; +} + +/* Okay, we're done with that. */ +#undef FT_ERRORDEF +#undef FT_ERROR_START_LIST +#undef FT_ERROR_END_LIST + +#include "solvespace.h" + +namespace SolveSpace { + +//----------------------------------------------------------------------------- +// Get the list of available font filenames, and load the name for each of +// them. Only that, though, not the glyphs too. +//----------------------------------------------------------------------------- +TtfFontList::TtfFontList() { + FT_Init_FreeType(&fontLibrary); +} + +TtfFontList::~TtfFontList() { + FT_Done_FreeType(fontLibrary); +} + +void TtfFontList::LoadAll() { + if(loaded) return; + + for(const Platform::Path &font : Platform::GetFontFiles()) { + TtfFont tf = {}; + tf.fontFile = font; + if(tf.LoadFromFile(fontLibrary)) + l.Add(&tf); + } + + // Add builtin font to end of font list so it is displayed first in the UI + { + TtfFont tf = {}; + tf.SetResourceID("fonts/BitstreamVeraSans-Roman-builtin.ttf"); + if(tf.LoadFromResource(fontLibrary)) + l.Add(&tf); + } + + // Sort fonts according to their actual name, not filename. + std::sort(l.begin(), l.end(), + [](const TtfFont &a, const TtfFont &b) { return a.name < b.name; }); + + // Filter out fonts with the same family and style name. This is not + // strictly necessarily the exact same font, but it will almost always be. + TtfFont *it = std::unique(l.begin(), l.end(), + [](const TtfFont &a, const TtfFont &b) { return a.name == b.name; }); + l.RemoveLast(&l[l.n] - it); + + loaded = true; +} + +TtfFont *TtfFontList::LoadFont(const std::string &font) +{ + LoadAll(); + + TtfFont *tf = std::find_if(l.begin(), l.end(), + [&font](const TtfFont &tf) { return tf.FontFileBaseName() == font; }); + + if(tf != l.end()) { + if(tf->fontFace == NULL) { + if(tf->IsResource()) + tf->LoadFromResource(fontLibrary, /*keepOpen=*/true); + else + tf->LoadFromFile(fontLibrary, /*keepOpen=*/true); + } + return tf; + } else { + return NULL; + } +} + +void TtfFontList::PlotString(const std::string &font, const std::string &str, + SBezierList *sbl, bool kerning, Vector origin, Vector u, Vector v) +{ + TtfFont *tf = LoadFont(font); + if(!str.empty() && tf != NULL) { + tf->PlotString(str, sbl, kerning, origin, u, v); + } else { + // No text or no font; so draw a big X for an error marker. + SBezier sb; + sb = SBezier::From(origin, origin.Plus(u).Plus(v)); + sbl->l.Add(&sb); + sb = SBezier::From(origin.Plus(v), origin.Plus(u)); + sbl->l.Add(&sb); + } +} + +double TtfFontList::AspectRatio(const std::string &font, const std::string &str, bool kerning) +{ + TtfFont *tf = LoadFont(font); + if(tf != NULL) { + return tf->AspectRatio(str, kerning); + } + + return 0.0; +} + +//----------------------------------------------------------------------------- +// Return the basename of our font filename; that's how the requests and +// entities that reference us will store it. +//----------------------------------------------------------------------------- +std::string TtfFont::FontFileBaseName() const { + return fontFile.FileName(); +} + +//----------------------------------------------------------------------------- +// Convenience method to set fontFile for resource-loaded fonts as res:// +//----------------------------------------------------------------------------- +void TtfFont::SetResourceID(const std::string &resource) { + fontFile = { "res://" + resource }; +} + +bool TtfFont::IsResource() const { + return fontFile.raw.compare(0, 6, "res://") == 0; +} + +//----------------------------------------------------------------------------- +// Load a TrueType font into memory. +//----------------------------------------------------------------------------- +bool TtfFont::LoadFromFile(FT_Library fontLibrary, bool keepOpen) { + ssassert(!IsResource(), "Cannot load a font provided by a resource as a file."); + + FT_Open_Args args = {}; + args.flags = FT_OPEN_PATHNAME; + args.pathname = &fontFile.raw[0]; // FT_String is char* for historical reasons + + // We don't use OpenFile() here to let freetype do its own memory management. + // This is OK because on Linux/OS X we just delegate to fopen and on Windows + // we only look into C:\Windows\Fonts, which has a known short path. + if(int fterr = FT_Open_Face(fontLibrary, &args, 0, &fontFace)) { + dbp("freetype: loading font from file '%s' failed: %s", + fontFile.raw.c_str(), ft_error_string(fterr)); + return false; + } + + return ExtractTTFData(keepOpen); +} + +//----------------------------------------------------------------------------- +// Load a TrueType from resource in memory. Implemented to load bundled fonts +// through theresource system. +//----------------------------------------------------------------------------- +bool TtfFont::LoadFromResource(FT_Library fontLibrary, bool keepOpen) { + ssassert(IsResource(), "Font to be loaded as resource is not provided by a resource " + "or does not have the 'res://' prefix."); + + size_t _size; + // substr to cut off 'res://' (length: 6) + const void *_buffer = Platform::LoadResource(fontFile.raw.substr(6, fontFile.raw.size()), + &_size); + + FT_Long size = static_cast(_size); + const FT_Byte *buffer = reinterpret_cast(_buffer); + + if(int fterr = FT_New_Memory_Face(fontLibrary, buffer, size, 0, &fontFace)) { + dbp("freetype: loading font '%s' from memory failed: %s", + fontFile.raw.c_str(), ft_error_string(fterr)); + return false; + } + + return ExtractTTFData(keepOpen); +} + +//----------------------------------------------------------------------------- +// Extract font information. We care about the font name and unit size. +//----------------------------------------------------------------------------- +bool TtfFont::ExtractTTFData(bool keepOpen) { + if(int fterr = FT_Select_Charmap(fontFace, FT_ENCODING_UNICODE)) { + dbp("freetype: loading unicode CMap for file '%s' failed: %s", + fontFile.raw.c_str(), ft_error_string(fterr)); + FT_Done_Face(fontFace); + fontFace = NULL; + return false; + } + + name = std::string(fontFace->family_name) + + " (" + std::string(fontFace->style_name) + ")"; + + // We always ask Freetype to give us a unit size character. + // It uses fixed point; put the unit size somewhere in the middle of the dynamic + // range of its 26.6 fixed point type, and adjust the factors so that the unit + // matches cap height. + FT_Size_RequestRec sizeRequest; + sizeRequest.type = FT_SIZE_REQUEST_TYPE_REAL_DIM; + sizeRequest.width = 1 << 16; + sizeRequest.height = 1 << 16; + sizeRequest.horiResolution = 128; + sizeRequest.vertResolution = 128; + if(int fterr = FT_Request_Size(fontFace, &sizeRequest)) { + dbp("freetype: size request for file '%s' failed: %s", + fontFile.raw.c_str(), ft_error_string(fterr)); + FT_Done_Face(fontFace); + fontFace = NULL; + return false; + } + + char chr = 'A'; + uint32_t gid = FT_Get_Char_Index(fontFace, 'A'); + if (gid == 0) { + dbp("freetype: CID-to-GID mapping for CID 0x%04x in file '%s' failed: %s; " + "using CID as GID", + chr, fontFile.raw.c_str(), ft_error_string(gid)); + dbp("Assuming cap height is the same as requested height (this is likely wrong)."); + capHeight = (double)sizeRequest.height; + } + + if(gid) { + if(int fterr = FT_Load_Glyph(fontFace, gid, FT_LOAD_NO_BITMAP | FT_LOAD_NO_HINTING)) { + dbp("freetype: cannot load glyph for GID 0x%04x in file '%s': %s", + gid, fontFile.raw.c_str(), ft_error_string(fterr)); + FT_Done_Face(fontFace); + fontFace = NULL; + return false; + } + + FT_BBox bbox; + FT_Outline_Get_CBox(&fontFace->glyph->outline, &bbox); + capHeight = (double)bbox.yMax; + } + + // If we just wanted to get the font's name and figure out if it's actually usable, close + // it now. If we don't do this, and there are a lot of fonts, we can bump into the file + // descriptor limit (especially on Windows), breaking all file operations. + if(!keepOpen) { + FT_Done_Face(fontFace); + fontFace = NULL; + return true; + } + + return true; +} + +typedef struct OutlineData { + Vector origin, u, v; // input parameters + SBezierList *beziers; // output bezier list + float factor; // ratio between freetype and solvespace coordinates + FT_Pos bx; // x offset of the current glyph + FT_Pos px, py; // current point +} OutlineData; + +static Vector Transform(OutlineData *data, FT_Pos x, FT_Pos y) { + Vector r = data->origin; + r = r.Plus(data->u.ScaledBy((float)(data->bx + x) * data->factor)); + r = r.Plus(data->v.ScaledBy((float)y * data->factor)); + return r; +} + +static int MoveTo(const FT_Vector *p, void *cc) +{ + OutlineData *data = (OutlineData *) cc; + data->px = p->x; + data->py = p->y; + return 0; +} + +static int LineTo(const FT_Vector *p, void *cc) +{ + OutlineData *data = (OutlineData *) cc; + SBezier sb = SBezier::From( + Transform(data, data->px, data->py), + Transform(data, p->x, p->y)); + data->beziers->l.Add(&sb); + data->px = p->x; + data->py = p->y; + return 0; +} + +static int ConicTo(const FT_Vector *c, const FT_Vector *p, void *cc) +{ + OutlineData *data = (OutlineData *) cc; + SBezier sb = SBezier::From( + Transform(data, data->px, data->py), + Transform(data, c->x, c->y), + Transform(data, p->x, p->y)); + data->beziers->l.Add(&sb); + data->px = p->x; + data->py = p->y; + return 0; +} + +static int CubicTo(const FT_Vector *c1, const FT_Vector *c2, const FT_Vector *p, void *cc) +{ + OutlineData *data = (OutlineData *) cc; + SBezier sb = SBezier::From( + Transform(data, data->px, data->py), + Transform(data, c1->x, c1->y), + Transform(data, c2->x, c2->y), + Transform(data, p->x, p->y)); + data->beziers->l.Add(&sb); + data->px = p->x; + data->py = p->y; + return 0; +} + +void TtfFont::PlotString(const std::string &str, + SBezierList *sbl, bool kerning, Vector origin, Vector u, Vector v) +{ + ssassert(fontFace != NULL, "Expected font face to be loaded"); + + FT_Outline_Funcs outlineFuncs; + outlineFuncs.move_to = MoveTo; + outlineFuncs.line_to = LineTo; + outlineFuncs.conic_to = ConicTo; + outlineFuncs.cubic_to = CubicTo; + outlineFuncs.shift = 0; + outlineFuncs.delta = 0; + + FT_Pos dx = 0; + uint32_t prevGid = 0; + for(char32_t cid : ReadUTF8(str)) { + uint32_t gid = FT_Get_Char_Index(fontFace, cid); + if (gid == 0) { + dbp("freetype: CID-to-GID mapping for CID 0x%04x in file '%s' failed: %s; " + "using CID as GID", + cid, fontFile.raw.c_str(), ft_error_string(gid)); + gid = cid; + } + + /* + * Stupid hacks: + * - if we want fake-bold, use FT_Outline_Embolden(). This actually looks + * quite good. + * - if we want fake-italic, apply a shear transform [1 s s 1 0 0] here using + * FT_Set_Transform. This looks decent at small font sizes and bad at larger + * ones, antialiasing mitigates this considerably though. + */ + if(int fterr = FT_Load_Glyph(fontFace, gid, FT_LOAD_NO_BITMAP | FT_LOAD_NO_HINTING)) { + dbp("freetype: cannot load glyph for GID 0x%04x in file '%s': %s", + gid, fontFile.raw.c_str(), ft_error_string(fterr)); + return; + } + + /* A point that has x = xMin should be plotted at (dx0 + lsb); fix up + * our x-position so that the curve-generating code will put stuff + * at the right place. + * + * There's no point in getting the glyph BBox here - not only can it be + * needlessly slow sometimes, but because we're about to render a single glyph, + * what we want actually *is* the CBox. + * + * This is notwithstanding that this makes extremely little sense, this + * looks like a workaround for either mishandling the start glyph on a line, + * or as a really hacky pseudo-track-kerning (in which case it works better than + * one would expect! especially since most fonts don't set track kerning). + */ + FT_BBox cbox; + FT_Outline_Get_CBox(&fontFace->glyph->outline, &cbox); + + // Apply Kerning, if any: + FT_Vector kernVector; + if(kerning && FT_Get_Kerning(fontFace, prevGid, gid, FT_KERNING_DEFAULT, &kernVector) == 0) { + dx += kernVector.x; + } + + FT_Pos bx = dx - cbox.xMin; + // Yes, this is what FreeType calls left-side bearing. + // Then interchangeably uses that with "left-side bearing". Sigh. + bx += fontFace->glyph->metrics.horiBearingX; + + OutlineData data = {}; + data.origin = origin; + data.u = u; + data.v = v; + data.beziers = sbl; + data.factor = (float)(1.0 / capHeight); + data.bx = bx; + if(int fterr = FT_Outline_Decompose(&fontFace->glyph->outline, &outlineFuncs, &data)) { + dbp("freetype: bezier decomposition failed for GID 0x%4x in file '%s': %s", + gid, fontFile.raw.c_str(), ft_error_string(fterr)); + } + + // And we're done, so advance our position by the requested advance + // width, plus the user-requested extra advance. + dx += fontFace->glyph->advance.x; + prevGid = gid; + } +} + +double TtfFont::AspectRatio(const std::string &str, bool kerning) { + ssassert(fontFace != NULL, "Expected font face to be loaded"); + + // We always request a unit size character, so the aspect ratio is the same as advance length. + double dx = 0; + uint32_t prevGid = 0; + for(char32_t chr : ReadUTF8(str)) { + uint32_t gid = FT_Get_Char_Index(fontFace, chr); + if (gid == 0) { + dbp("freetype: CID-to-GID mapping for CID 0x%04x in file '%s' failed: %s; " + "using CID as GID", + chr, fontFile.raw.c_str(), ft_error_string(gid)); + } + + if(int fterr = FT_Load_Glyph(fontFace, gid, FT_LOAD_NO_BITMAP | FT_LOAD_NO_HINTING)) { + dbp("freetype: cannot load glyph for GID 0x%04x in file '%s': %s", + gid, fontFile.raw.c_str(), ft_error_string(fterr)); + break; + } + + // Apply Kerning, if any: + FT_Vector kernVector; + if(kerning && FT_Get_Kerning(fontFace, prevGid, gid, FT_KERNING_DEFAULT, &kernVector) == 0) { + dx += (double)kernVector.x / capHeight; + } + + dx += (double)fontFace->glyph->advance.x / capHeight; + prevGid = gid; + } + + return dx; +} + +} // namespace SolveSpace diff --git a/src/ttf.h b/src/ttf.h new file mode 100644 index 0000000..560158d --- /dev/null +++ b/src/ttf.h @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------------- +// Routines to read a TrueType font as vector outlines, and generate them +// as entities, since they're always representable as either lines or +// quadratic Bezier curves. +// +// Copyright 2016 whitequark, Peter Barfuss. +//----------------------------------------------------------------------------- + +#ifndef SOLVESPACE_TTF_H +#define SOLVESPACE_TTF_H + +#include + +#include "dsc.h" +#include "platform/platform.h" + +// We declare these in advance instead of simply using FT_Library +// (defined as typedef FT_LibraryRec_* FT_Library) because including +// freetype.h invokes indescribable horrors and we would like to avoid +// doing that every time we include solvespace.h. +struct FT_LibraryRec_; +struct FT_FaceRec_; + +namespace SolveSpace { + +class SBezierList; + +class TtfFont { +public: + Platform::Path fontFile; // or resource path/name as res:// + std::string name; + FT_FaceRec_ *fontFace; + double capHeight; + + void SetResourceID(const std::string &resource); + bool IsResource() const; + + std::string FontFileBaseName() const; + bool LoadFromFile(FT_LibraryRec_ *fontLibrary, bool keepOpen = false); + bool LoadFromResource(FT_LibraryRec_ *fontLibrary, bool keepOpen = false); + + void PlotString(const std::string &str, + SBezierList *sbl, bool kerning, Vector origin, Vector u, Vector v); + double AspectRatio(const std::string &str, bool kerning); + + bool ExtractTTFData(bool keepOpen); +}; + +class TtfFontList { +public: + FT_LibraryRec_ *fontLibrary; + bool loaded; + List l; + + TtfFontList(); + ~TtfFontList(); + + void LoadAll(); + TtfFont *LoadFont(const std::string &font); + + void PlotString(const std::string &font, const std::string &str, + SBezierList *sbl, bool kerning, Vector origin, Vector u, Vector v); + double AspectRatio(const std::string &font, const std::string &str, bool kerning); +}; + +} // namespace SolveSpace + +#endif diff --git a/src/ui.h b/src/ui.h new file mode 100644 index 0000000..3bb789a --- /dev/null +++ b/src/ui.h @@ -0,0 +1,889 @@ +//----------------------------------------------------------------------------- +// Declarations relating to our user interface, in both the graphics and +// text browser window. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- + +#ifndef SOLVESPACE_UI_H +#define SOLVESPACE_UI_H + +#include +#include +#include +#include +#include + +#include "dsc.h" +#include "platform/gui.h" +#include "sketch.h" + +namespace SolveSpace { + +class Camera; +class Lighting; +class Canvas; +class UiCanvas; +class BatchCanvas; +class ViewportCanvas; +class Pixmap; + +class Locale { +public: + std::string language; + std::string region; + uint16_t lcid; + std::string displayName; + + std::string Culture() const { + return language + "-" + region; + } +}; + +struct LocaleLess { + bool operator()(const Locale &a, const Locale &b) const { + return a.language < b.language || + (a.language == b.language && a.region < b.region); + } +}; + +const std::set &Locales(); +bool SetLocale(const std::string &name); +bool SetLocale(uint16_t lcid); + +const std::string &Translate(const char *msgid); +const std::string &Translate(const char *msgctxt, const char *msgid); +const std::string &TranslatePlural(const char *msgid, unsigned n); +const std::string &TranslatePlural(const char *msgctxt, const char *msgid, unsigned n); + +inline const char *N_(const char *msgid) { + return msgid; +} +inline const char *CN_(const char *msgctxt, const char *msgid) { + return msgid; +} +#if defined(LIBRARY) +inline const char *_(const char *msgid) { + return msgid; +} +inline const char *C_(const char *msgctxt, const char *msgid) { + return msgid; +} +#else +inline const char *_(const char *msgid) { + return Translate(msgid).c_str(); +} +inline const char *C_(const char *msgctxt, const char *msgid) { + return Translate(msgctxt, msgid).c_str(); +} +#endif + +// This table describes the top-level menus in the graphics window. +enum class Command : uint32_t { + NONE = 0, + // File + NEW = 100, + OPEN, + OPEN_RECENT, + SAVE, + SAVE_AS, + EXPORT_IMAGE, + EXPORT_MESH, + EXPORT_SURFACES, + EXPORT_VIEW, + EXPORT_SECTION, + EXPORT_WIREFRAME, + IMPORT, + EXIT, + // View + ZOOM_IN, + ZOOM_OUT, + ZOOM_TO_FIT, + SHOW_GRID, + DIM_SOLID_MODEL, + PERSPECTIVE_PROJ, + EXPLODE_SKETCH, + ONTO_WORKPLANE, + NEAREST_ORTHO, + NEAREST_ISO, + CENTER_VIEW, + SHOW_TOOLBAR, + SHOW_TEXT_WND, + UNITS_INCHES, + UNITS_FEET_INCHES, + UNITS_MM, + UNITS_METERS, + FULL_SCREEN, + // Edit + UNDO, + REDO, + CUT, + COPY, + PASTE, + PASTE_TRANSFORM, + DELETE, + SELECT_CHAIN, + SELECT_ALL, + SNAP_TO_GRID, + ROTATE_90, + UNSELECT_ALL, + REGEN_ALL, + EDIT_LINE_STYLES, + VIEW_PROJECTION, + CONFIGURATION, + // Request + SEL_WORKPLANE, + FREE_IN_3D, + DATUM_POINT, + WORKPLANE, + LINE_SEGMENT, + CONSTR_SEGMENT, + CIRCLE, + ARC, + RECTANGLE, + CUBIC, + TTF_TEXT, + IMAGE, + SPLIT_CURVES, + TANGENT_ARC, + CONSTRUCTION, + // Group + GROUP_3D, + GROUP_WRKPL, + GROUP_EXTRUDE, + GROUP_HELIX, + GROUP_LATHE, + GROUP_REVOLVE, + GROUP_ROT, + GROUP_TRANS, + GROUP_LINK, + GROUP_RECENT, + // Constrain + DISTANCE_DIA, + REF_DISTANCE, + ANGLE, + REF_ANGLE, + OTHER_ANGLE, + REFERENCE, + EQUAL, + RATIO, + DIFFERENCE, + ON_ENTITY, + SYMMETRIC, + AT_MIDPOINT, + HORIZONTAL, + VERTICAL, + PARALLEL, + PERPENDICULAR, + ORIENTED_SAME, + WHERE_DRAGGED, + COMMENT, + // Analyze + VOLUME, + AREA, + PERIMETER, + INTERFERENCE, + NAKED_EDGES, + SHOW_DOF, + CENTER_OF_MASS, + TRACE_PT, + STOP_TRACING, + STEP_DIM, + // Help + LOCALE, + WEBSITE, + GITHUB, + ABOUT, +}; + +class Button; + +class TextWindow { +public: + enum { + MAX_COLS = 100, + MIN_COLS = 45, + MAX_ROWS = 4000 + }; + + typedef struct { + char c; + RgbaColor color; + } Color; + static const Color fgColors[]; + static const Color bgColors[]; + + float bgColorTable[256*3]; + float fgColorTable[256*3]; + + enum { + CHAR_WIDTH_ = 9, + CHAR_HEIGHT = 16, + LINE_HEIGHT = 20, + LEFT_MARGIN = 6, + }; + +#define CHECK_FALSE "\xEE\x80\x80" // U+E000 +#define CHECK_TRUE "\xEE\x80\x81" +#define RADIO_FALSE "\xEE\x80\x82" +#define RADIO_TRUE "\xEE\x80\x83" + + int scrollPos; // The scrollbar position, in half-row units + int halfRows; // The height of our window, in half-row units + + uint32_t text[MAX_ROWS][MAX_COLS]; + typedef void LinkFunction(int link, uint32_t v); + enum { NOT_A_LINK = 0 }; + struct { + char fg; + char bg; + RgbaColor bgRgb; + int link; + uint32_t data; + LinkFunction *f; + LinkFunction *h; + } meta[MAX_ROWS][MAX_COLS]; + int hoveredRow, hoveredCol; + + int top[MAX_ROWS]; // in half-line units, or -1 for unused + int rows; + + Platform::WindowRef window; + std::shared_ptr canvas; + + void Draw(Canvas *canvas); + + void Paint(); + void MouseEvent(bool isClick, bool leftDown, double x, double y); + void MouseLeave(); + void ScrollbarEvent(double newPos); + + enum DrawOrHitHow : uint32_t { + PAINT = 0, + HOVER = 1, + CLICK = 2 + }; + void DrawOrHitTestIcons(UiCanvas *canvas, DrawOrHitHow how, + double mx, double my); + Button *hoveredButton; + + Vector HsvToRgb(Vector hsv); + std::shared_ptr HsvPattern2d(int w, int h); + std::shared_ptr HsvPattern1d(double hue, double sat, int w, int h); + void ColorPickerDone(); + bool DrawOrHitTestColorPicker(UiCanvas *canvas, DrawOrHitHow how, + bool leftDown, double x, double y); + + void Init(); + void MakeColorTable(const Color *in, float *out); + void Printf(bool half, const char *fmt, ...); + void ClearScreen(); + + void Show(); + void Resize(); + + // State for the screen that we are showing in the text window. + enum class Screen : uint32_t { + LIST_OF_GROUPS = 0, + GROUP_INFO = 1, + GROUP_SOLVE_INFO = 2, + CONFIGURATION = 3, + STEP_DIMENSION = 4, + LIST_OF_STYLES = 5, + STYLE_INFO = 6, + PASTE_TRANSFORMED = 7, + EDIT_VIEW = 8, + TANGENT_ARC = 9 + }; + typedef struct { + Screen screen; + + hGroup group; + hStyle style; + + hConstraint constraint; + + struct { + int times; + Vector trans; + double theta; + Vector origin; + double scale; + } paste; + } ShownState; + ShownState shown; + + enum class Edit : uint32_t { + NOTHING = 0, + // For multiple groups + TIMES_REPEATED = 1, + GROUP_NAME = 2, + GROUP_SCALE = 3, + GROUP_COLOR = 4, + GROUP_OPACITY = 5, + // For the configuration screen + LIGHT_DIRECTION = 100, + LIGHT_INTENSITY = 101, + COLOR = 102, + CHORD_TOLERANCE = 103, + MAX_SEGMENTS = 104, + CAMERA_TANGENT = 105, + GRID_SPACING = 106, + DIGITS_AFTER_DECIMAL = 107, + DIGITS_AFTER_DECIMAL_DEGREE = 108, + EXPORT_SCALE = 109, + EXPORT_OFFSET = 110, + CANVAS_SIZE = 111, + G_CODE_DEPTH = 112, + G_CODE_SAFE_HEIGHT = 113, + G_CODE_PASSES = 114, + G_CODE_FEED = 115, + G_CODE_PLUNGE_FEED = 116, + AUTOSAVE_INTERVAL = 117, + LIGHT_AMBIENT = 118, + FIND_CONSTRAINT_TIMEOUT = 119, + EXPLODE_DISTANCE = 120, + ANIMATION_SPEED = 121, + // For TTF text + TTF_TEXT = 300, + // For the step dimension screen + STEP_DIM_FINISH = 400, + STEP_DIM_STEPS = 401, + // For the styles stuff + STYLE_WIDTH = 500, + STYLE_TEXT_HEIGHT = 501, + STYLE_TEXT_ANGLE = 502, + STYLE_COLOR = 503, + STYLE_FILL_COLOR = 504, + STYLE_NAME = 505, + BACKGROUND_COLOR = 506, + STYLE_STIPPLE_PERIOD = 508, + // For paste transforming + PASTE_TIMES_REPEATED = 600, + PASTE_ANGLE = 601, + PASTE_SCALE = 602, + // For view + VIEW_SCALE = 700, + VIEW_ORIGIN = 701, + VIEW_PROJ_RIGHT = 702, + VIEW_PROJ_UP = 703, + // For tangent arc + TANGENT_ARC_RADIUS = 800, + // For helix pitch + HELIX_PITCH = 802 + }; + struct { + bool showAgain; + Edit meaning; + int i; + hGroup group; + hRequest request; + hStyle style; + } edit; + + static void ReportHowGroupSolved(hGroup hg); + + struct { + int halfRow; + int col; + + struct { + RgbaColor rgb; + double h, s, v; + bool show; + bool picker1dActive; + bool picker2dActive; + } colorPicker; + } editControl; + + void HideEditControl(); + void ShowEditControl(int col, const std::string &str, int halfRow = -1); + void ShowEditControlWithColorPicker(int col, RgbaColor rgb); + + void ClearSuper(); + + void ShowHeader(bool withNav); + // These are self-contained screens, that show some information about + // the sketch. + void ShowListOfGroups(); + void ShowGroupInfo(); + void ShowGroupSolveInfo(); + void ShowConfiguration(); + void ShowListOfStyles(); + void ShowStyleInfo(); + void ShowStepDimension(); + void ShowPasteTransformed(); + void ShowEditView(); + void ShowTangentArc(); + // Special screen, based on selection + void DescribeSelection(); + + void GoToScreen(Screen screen); + + // All of these are callbacks from the GUI code; first from when + // we're describing an entity + static void ScreenEditTtfText(int link, uint32_t v); + static void ScreenToggleTtfKerning(int link, uint32_t v); + static void ScreenSetTtfFont(int link, uint32_t v); + static void ScreenUnselectAll(int link, uint32_t v); + + // when we're describing a constraint + static void ScreenConstraintToggleReference(int link, uint32_t v); + static void ScreenConstraintShowAsRadius(int link, uint32_t v); + + // and the rest from the stuff in textscreens.cpp + static void ScreenSelectGroup(int link, uint32_t v); + static void ScreenActivateGroup(int link, uint32_t v); + static void ScreenToggleGroupShown(int link, uint32_t v); + static void ScreenHowGroupSolved(int link, uint32_t v); + static void ScreenShowGroupsSpecial(int link, uint32_t v); + static void ScreenDeleteGroup(int link, uint32_t v); + + static void ScreenHoverGroupWorkplane(int link, uint32_t v); + static void ScreenHoverRequest(int link, uint32_t v); + static void ScreenHoverEntity(int link, uint32_t v); + static void ScreenHoverConstraint(int link, uint32_t v); + static void ScreenSelectRequest(int link, uint32_t v); + static void ScreenSelectEntity(int link, uint32_t v); + static void ScreenSelectConstraint(int link, uint32_t v); + + static void ScreenChangeGroupOption(int link, uint32_t v); + static void ScreenColor(int link, uint32_t v); + static void ScreenOpacity(int link, uint32_t v); + + static void ScreenShowListOfStyles(int link, uint32_t v); + static void ScreenShowStyleInfo(int link, uint32_t v); + static void ScreenDeleteStyle(int link, uint32_t v); + static void ScreenChangeStylePatternType(int link, uint32_t v); + static void ScreenChangeStyleYesNo(int link, uint32_t v); + static void ScreenCreateCustomStyle(int link, uint32_t v); + static void ScreenLoadFactoryDefaultStyles(int link, uint32_t v); + static void ScreenAssignSelectionToStyle(int link, uint32_t v); + + static void ScreenShowConfiguration(int link, uint32_t v); + static void ScreenShowEditView(int link, uint32_t v); + static void ScreenGoToWebsite(int link, uint32_t v); + + static void ScreenChangeArcDimDefault(int link, uint32_t v); + static void ScreenChangeShowFullFilePath(int link, uint32_t v); + static void ScreenChangeFixExportColors(int link, uint32_t v); + static void ScreenChangeExportBackgroundColor(int link, uint32_t v); + static void ScreenChangeBackFaces(int link, uint32_t v); + static void ScreenChangeShowContourAreas(int link, uint32_t v); + static void ScreenChangeCheckClosedContour(int link, uint32_t v); + static void ScreenChangeCameraNav(int link, uint32_t v); + static void ScreenChangeTurntableNav(int link, uint32_t v); + static void ScreenChangeImmediatelyEditDimension(int link, uint32_t v); + static void ScreenChangeAutomaticLineConstraints(int link, uint32_t v); + static void ScreenChangePwlCurves(int link, uint32_t v); + static void ScreenChangeCanvasSizeAuto(int link, uint32_t v); + static void ScreenChangeCanvasSize(int link, uint32_t v); + static void ScreenChangeShadedTriangles(int link, uint32_t v); + + static void ScreenAllowRedundant(int link, uint32_t v); + + struct { + bool isDistance; + double finish; + int steps; + + Platform::TimerRef timer; + int64_t time; + int step; + } stepDim; + static void ScreenStepDimSteps(int link, uint32_t v); + static void ScreenStepDimFinish(int link, uint32_t v); + static void ScreenStepDimGo(int link, uint32_t v); + + static void ScreenChangeTangentArc(int link, uint32_t v); + + static void ScreenPasteTransformed(int link, uint32_t v); + + static void ScreenHome(int link, uint32_t v); + + // These ones do stuff with the edit control + static void ScreenChangeExprA(int link, uint32_t v); + static void ScreenChangeGroupName(int link, uint32_t v); + static void ScreenChangeGroupScale(int link, uint32_t v); + static void ScreenChangeHelixPitch(int link, uint32_t v); + static void ScreenChangePitchOption(int link, uint32_t v); + static void ScreenChangeLightDirection(int link, uint32_t v); + static void ScreenChangeLightIntensity(int link, uint32_t v); + static void ScreenChangeLightAmbient(int link, uint32_t v); + static void ScreenChangeColor(int link, uint32_t v); + static void ScreenChangeChordTolerance(int link, uint32_t v); + static void ScreenChangeMaxSegments(int link, uint32_t v); + static void ScreenChangeExportChordTolerance(int link, uint32_t v); + static void ScreenChangeExportMaxSegments(int link, uint32_t v); + static void ScreenChangeCameraTangent(int link, uint32_t v); + static void ScreenChangeGridSpacing(int link, uint32_t v); + static void ScreenChangeExplodeDistance(int link, uint32_t v); + static void ScreenChangeDigitsAfterDecimal(int link, uint32_t v); + static void ScreenChangeDigitsAfterDecimalDegree(int link, uint32_t v); + static void ScreenChangeUseSIPrefixes(int link, uint32_t v); + static void ScreenChangeExportScale(int link, uint32_t v); + static void ScreenChangeExportOffset(int link, uint32_t v); + static void ScreenChangeGCodeParameter(int link, uint32_t v); + static void ScreenChangeAutosaveInterval(int link, uint32_t v); + static void ScreenChangeFindConstraintTimeout(int link, uint32_t v); + static void ScreenChangeAnimationSpeed(int link, uint32_t v); + static void ScreenChangeStyleName(int link, uint32_t v); + static void ScreenChangeStyleMetric(int link, uint32_t v); + static void ScreenChangeStyleTextAngle(int link, uint32_t v); + static void ScreenChangeStyleColor(int link, uint32_t v); + static void ScreenChangeBackgroundColor(int link, uint32_t v); + static void ScreenChangePasteTransformed(int link, uint32_t v); + static void ScreenChangeViewScale(int link, uint32_t v); + static void ScreenChangeViewToFullScale(int link, uint32_t v); + static void ScreenChangeViewOrigin(int link, uint32_t v); + static void ScreenChangeViewProjection(int link, uint32_t v); + + bool EditControlDoneForStyles(const std::string &s); + bool EditControlDoneForConfiguration(const std::string &s); + bool EditControlDoneForPaste(const std::string &s); + bool EditControlDoneForView(const std::string &s); + void EditControlDone(std::string s); +}; + +class GraphicsWindow { +public: + void Init(); + + Platform::WindowRef window; + + void PopulateMainMenu(); + void PopulateRecentFiles(); + + Platform::KeyboardEvent AcceleratorForCommand(Command id); + void ActivateCommand(Command id); + + static void MenuView(Command id); + static void MenuEdit(Command id); + static void MenuRequest(Command id); + void DeleteSelection(); + void CopySelection(); + void PasteClipboard(Vector trans, double theta, double scale); + static void MenuClipboard(Command id); + + Platform::MenuRef openRecentMenu; + Platform::MenuRef linkRecentMenu; + + Platform::MenuItemRef showGridMenuItem; + Platform::MenuItemRef dimSolidModelMenuItem; + Platform::MenuItemRef perspectiveProjMenuItem; + Platform::MenuItemRef explodeMenuItem; + Platform::MenuItemRef showToolbarMenuItem; + Platform::MenuItemRef showTextWndMenuItem; + Platform::MenuItemRef fullScreenMenuItem; + + Platform::MenuItemRef unitsMmMenuItem; + Platform::MenuItemRef unitsMetersMenuItem; + Platform::MenuItemRef unitsInchesMenuItem; + Platform::MenuItemRef unitsFeetInchesMenuItem; + + Platform::MenuItemRef inWorkplaneMenuItem; + Platform::MenuItemRef in3dMenuItem; + + Platform::MenuItemRef undoMenuItem; + Platform::MenuItemRef redoMenuItem; + + std::shared_ptr canvas; + std::shared_ptr persistentCanvas; + bool persistentDirty; + + // These parameters define the map from 2d screen coordinates to the + // coordinates of the 3d sketch points. We will use an axonometric + // projection. + Vector offset; + Vector projRight; + Vector projUp; + double scale; + struct { + bool mouseDown; + Vector offset; + Vector projRight; + Vector projUp; + Point2d mouse; + Point2d mouseOnButtonDown; + Vector marqueePoint; + bool startedMoving; + } orig; + // We need to detect when the projection is changed to invalidate + // caches for drawn items. + struct { + Vector offset; + Vector projRight; + Vector projUp; + double scale; + } cached; + + // Most recent mouse position, updated every time the mouse moves. + Point2d currentMousePosition; + + // When the user is dragging a point, don't solve multiple times without + // allowing a paint in between. The extra solves are wasted if they're + // not displayed. + bool havePainted; + + // Some state for the context menu. + struct { + bool active; + } context; + + Camera GetCamera() const; + Lighting GetLighting() const; + + void NormalizeProjectionVectors(); + Point2d ProjectPoint(Vector p); + Vector ProjectPoint3(Vector p); + Vector ProjectPoint4(Vector p, double *w); + Vector UnProjectPoint(Point2d p); + Vector UnProjectPoint3(Vector p); + + Platform::TimerRef animateTimer; + void AnimateOnto(Quaternion quatf, Vector offsetf); + void AnimateOntoWorkplane(); + + Vector VectorFromProjs(Vector rightUpForward); + void HandlePointForZoomToFit(Vector p, Point2d *pmax, Point2d *pmin, + double *wmin, bool usePerspective, + const Camera &camera); + void ZoomToMouse(double delta); + void LoopOverPoints(const std::vector &entities, + const std::vector &constraints, + const std::vector &faces, + Point2d *pmax, Point2d *pmin, + double *wmin, bool usePerspective, bool includeMesh, + const Camera &camera); + void ZoomToFit(bool includingInvisibles = false, bool useSelection = false); + double ZoomToFit(const Camera &camera, + bool includingInvisibles = false, bool useSelection = false); + + hGroup activeGroup; + void EnsureValidActives(); + bool LockedInWorkplane(); + void SetWorkplaneFreeIn3d(); + hEntity ActiveWorkplane(); + void ForceTextWindowShown(); + + // Operations that must be completed by doing something with the mouse + // are noted here. + enum class Pending : uint32_t { + NONE = 0, + COMMAND = 1, + DRAGGING_POINTS = 2, + DRAGGING_NEW_POINT = 3, + DRAGGING_NEW_LINE_POINT = 4, + DRAGGING_NEW_CUBIC_POINT = 5, + DRAGGING_NEW_ARC_POINT = 6, + DRAGGING_CONSTRAINT = 7, + DRAGGING_RADIUS = 8, + DRAGGING_NORMAL = 9, + DRAGGING_NEW_RADIUS = 10, + DRAGGING_MARQUEE = 11, + }; + + struct { + Pending operation; + Command command; + + hRequest request; + hEntity point; + List points; + List requests; + hEntity circle; + hEntity normal; + hConstraint constraint; + + const char *description; + Platform::Path filename; + + bool hasSuggestion; + Constraint::Type suggestion; + } pending; + void ClearPending(bool scheduleShowTW = true); + bool IsFromPending(hRequest r); + void AddToPending(hRequest r); + void ReplacePending(hRequest before, hRequest after); + + // The constraint that is being edited with the on-screen textbox. + hConstraint constraintBeingEdited; + + bool SuggestLineConstraint(hRequest lineSegment, ConstraintBase::Type *type); + + Vector SnapToGrid(Vector p); + Vector SnapToEntityByScreenPoint(Point2d pp, hEntity he); + bool ConstrainPointByHovered(hEntity pt, const Point2d *projected = NULL); + void DeleteTaggedRequests(); + hRequest AddRequest(Request::Type type, bool rememberForUndo); + hRequest AddRequest(Request::Type type); + + class ParametricCurve { + public: + bool isLine; // else circle + Vector p0, p1; + Vector u, v; + double r, theta0, theta1, dtheta; + + void MakeFromEntity(hEntity he, bool reverse); + Vector PointAt(double t); + Vector TangentAt(double t); + double LengthForAuto(); + + void CreateRequestTrimmedTo(double t, bool reuseOrig, + hEntity orig, hEntity arc, bool arcFinish, bool pointf); + void ConstrainPointIfCoincident(hEntity hpt); + }; + void MakeTangentArc(); + void SplitLinesOrCurves(); + hEntity SplitEntity(hEntity he, Vector pinter); + hEntity SplitLine(hEntity he, Vector pinter); + hEntity SplitCircle(hEntity he, Vector pinter); + hEntity SplitCubic(hEntity he, Vector pinter); + void ReplacePointInConstraints(hEntity oldpt, hEntity newpt); + void RemoveConstraintsForPointBeingDeleted(hEntity hpt); + void FixConstraintsForRequestBeingDeleted(hRequest hr); + void FixConstraintsForPointBeingDeleted(hEntity hpt); + void EditConstraint(hConstraint constraint); + + // A selected entity. + class Selection { + public: + int tag; + + hEntity entity; + hConstraint constraint; + bool emphasized; + + void Draw(bool isHovered, Canvas *canvas); + + void Clear(); + bool IsEmpty(); + bool Equals(Selection *b); + bool HasEndpoints(); + }; + + // A hovered entity, with its location relative to the cursor. + class Hover { + public: + int zIndex; + double distance; + double depth; + Selection selection; + }; + + List hoverList; + Selection hover; + bool hoverWasSelectedOnMousedown; + List selection; + const unsigned MAX_SELECTABLE_FACES = 3u; + + Selection ChooseFromHoverToSelect(); + Selection ChooseFromHoverToDrag(); + void HitTestMakeSelection(Point2d mp); + void ClearSelection(); + void ClearNonexistentSelectionItems(); + /// This structure is filled by a call to GroupSelection(). + struct { + std::vector point; + std::vector entity; + std::vector anyNormal; + std::vector vector; + std::vector face; + std::vector constraint; + int points; + int entities; + int workplanes; + int faces; + int lineSegments; + int circlesOrArcs; + int arcs; + int cubics; + int periodicCubics; + int anyNormals; + int vectors; + int constraints; + int stylables; + int constraintLabels; + int withEndpoints; + int n; ///< Number of selected items + } gs; + void GroupSelection(); + bool IsSelected(Selection *s); + bool IsSelected(hEntity he); + void MakeSelected(hEntity he); + void MakeSelected(hConstraint hc); + void MakeSelected(Selection *s); + void MakeUnselected(hEntity he, bool coincidentPointTrick); + void MakeUnselected(Selection *s, bool coincidentPointTrick); + void SelectByMarquee(); + void ClearSuper(); + + // The toolbar, in toolbar.cpp + bool ToolbarDrawOrHitTest(int x, int y, UiCanvas *canvas, + Command *hitCommand, int *hitX, int *hitY); + void ToolbarDraw(UiCanvas *canvas); + bool ToolbarMouseMoved(int x, int y); + bool ToolbarMouseDown(int x, int y); + Command toolbarHovered; + + + // This sets what gets displayed. + bool showWorkplanes; + bool showNormals; + bool showPoints; + bool showConstruction; + + enum class ShowConstraintMode : unsigned { SCM_NOSHOW, SCM_SHOW_ALL, SCM_SHOW_DIM }; + ShowConstraintMode showConstraints; + + bool showTextWindow; + bool showShaded; + bool showEdges; + bool showOutlines; + bool showFaces; + bool showFacesDrawing; + bool showFacesNonDrawing; + bool showMesh; + void ToggleBool(bool *v); + + enum class DrawOccludedAs : unsigned { INVISIBLE, STIPPLED, VISIBLE }; + DrawOccludedAs drawOccludedAs; + + bool showSnapGrid; + bool dimSolidModel; + void DrawSnapGrid(Canvas *canvas); + + void AddPointToDraggedList(hEntity hp); + void StartDraggingByEntity(hEntity he); + void StartDraggingBySelection(); + void UpdateDraggedNum(Vector *pos, double mx, double my); + void UpdateDraggedPoint(hEntity hp, double mx, double my); + + void Invalidate(bool clearPersistent = false); + void DrawEntities(Canvas *canvas, bool persistent); + void DrawPersistent(Canvas *canvas); + void Draw(Canvas *canvas); + void Paint(); + + bool MouseEvent(Platform::MouseEvent event); + void MouseMoved(double x, double y, bool leftDown, bool middleDown, + bool rightDown, bool shiftDown, bool ctrlDown); + void MouseLeftDown(double x, double y, bool shiftDown, bool ctrlDown); + void MouseLeftUp(double x, double y, bool shiftDown, bool ctrlDown); + void MouseLeftDoubleClick(double x, double y); + void MouseMiddleOrRightDown(double x, double y); + void MouseRightUp(double x, double y); + void MouseScroll(double delta); + void MouseLeave(); + bool KeyboardEvent(Platform::KeyboardEvent event); + void EditControlDone(const std::string &s); + + int64_t last6DofTime; + hGroup last6DofGroup; + void SixDofEvent(Platform::SixDofEvent event); +}; + +} // namespace SolveSpace + +#endif diff --git a/src/undoredo.cpp b/src/undoredo.cpp new file mode 100644 index 0000000..17d1a37 --- /dev/null +++ b/src/undoredo.cpp @@ -0,0 +1,157 @@ +//----------------------------------------------------------------------------- +// The user-visible undo/redo operation; whenever they change something, we +// record our state and push it on a stack, and we pop the stack when they +// select undo. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +void SolveSpaceUI::UndoRemember() { + unsaved = true; + PushFromCurrentOnto(&undo); + UndoClearStack(&redo); + UndoEnableMenus(); +} + +void SolveSpaceUI::UndoUndo() { + if(undo.cnt <= 0) return; + + PushFromCurrentOnto(&redo); + PopOntoCurrentFrom(&undo); + UndoEnableMenus(); +} + +void SolveSpaceUI::UndoRedo() { + if(redo.cnt <= 0) return; + + PushFromCurrentOnto(&undo); + PopOntoCurrentFrom(&redo); + UndoEnableMenus(); +} + +void SolveSpaceUI::UndoEnableMenus() { + SS.GW.undoMenuItem->SetEnabled(undo.cnt > 0); + SS.GW.redoMenuItem->SetEnabled(redo.cnt > 0); +} + +void SolveSpaceUI::PushFromCurrentOnto(UndoStack *uk) { + if(uk->cnt == MAX_UNDO) { + UndoClearState(&(uk->d[uk->write])); + // And then write in to this one again + } else { + (uk->cnt)++; + } + + UndoState *ut = &(uk->d[uk->write]); + *ut = {}; + ut->group.ReserveMore(SK.group.n); + for(Group &src : SK.group) { + // Shallow copy + Group dest(src); + // And then clean up all the stuff that needs to be a deep copy, + // and zero out all the dynamic stuff that will get regenerated. + dest.clean = false; + dest.solved = {}; + dest.polyLoops = {}; + dest.bezierLoops = {}; + dest.bezierOpens = {}; + dest.polyError = {}; + dest.thisMesh = {}; + dest.runningMesh = {}; + dest.thisShell = {}; + dest.runningShell = {}; + dest.displayMesh = {}; + dest.displayOutlines = {}; + + dest.remap = src.remap; + + dest.impMesh = {}; + dest.impShell = {}; + dest.impEntity = {}; + ut->group.Add(&dest); + } + for(auto &src : SK.groupOrder) { ut->groupOrder.Add(&src); } + ut->request.ReserveMore(SK.request.n); + for(auto &src : SK.request) { ut->request.Add(&src); } + ut->constraint.ReserveMore(SK.constraint.n); + for(auto &src : SK.constraint) { + // Shallow copy + Constraint dest(src); + ut->constraint.Add(&dest); + } + ut->param.ReserveMore(SK.param.n); + for(auto &src : SK.param) { ut->param.Add(&src); } + ut->style.ReserveMore(SK.style.n); + for(auto &src : SK.style) { ut->style.Add(&src); } + ut->activeGroup = SS.GW.activeGroup; + + uk->write = WRAP(uk->write + 1, MAX_UNDO); +} + +void SolveSpaceUI::PopOntoCurrentFrom(UndoStack *uk) { + ssassert(uk->cnt > 0, "Cannot pop from empty undo stack"); + (uk->cnt)--; + uk->write = WRAP(uk->write - 1, MAX_UNDO); + + UndoState *ut = &(uk->d[uk->write]); + + // Free everything in the main copy of the program before replacing it + for(hGroup hg : SK.groupOrder) { + Group *g = SK.GetGroup(hg); + g->Clear(); + } + SK.group.Clear(); + SK.groupOrder.Clear(); + SK.request.Clear(); + SK.constraint.Clear(); + SK.param.Clear(); + SK.style.Clear(); + + // And then do a shallow copy of the state from the undo list + ut->group.MoveSelfInto(&(SK.group)); + for(auto &gh : ut->groupOrder) { SK.groupOrder.Add(&gh); } + ut->request.MoveSelfInto(&(SK.request)); + ut->constraint.MoveSelfInto(&(SK.constraint)); + ut->param.MoveSelfInto(&(SK.param)); + ut->style.MoveSelfInto(&(SK.style)); + SS.GW.activeGroup = ut->activeGroup; + + // No need to free it, since a shallow copy was made above + *ut = {}; + + // And reset the state everywhere else in the program, since the + // sketch just changed a lot. + SS.GW.ClearSuper(); + SS.TW.ClearSuper(); + SS.ReloadAllLinked(SS.saveFile); + SS.GenerateAll(SolveSpaceUI::Generate::ALL); + SS.ScheduleShowTW(); + + // Activate the group that was active before. + Group *activeGroup = SK.GetGroup(SS.GW.activeGroup); + activeGroup->Activate(); +} + +void SolveSpaceUI::UndoClearStack(UndoStack *uk) { + while(uk->cnt > 0) { + uk->write = WRAP(uk->write - 1, MAX_UNDO); + (uk->cnt)--; + UndoClearState(&(uk->d[uk->write])); + } + *uk = {}; // for good measure +} + +void SolveSpaceUI::UndoClearState(UndoState *ut) { + for(auto &g : ut->group) { g.remap.clear(); } + ut->group.Clear(); + ut->request.Clear(); + ut->constraint.Clear(); + ut->param.Clear(); + ut->style.Clear(); + *ut = {}; +} + +} // namespace SolveSpace diff --git a/src/util.cpp b/src/util.cpp new file mode 100644 index 0000000..d1ea15a --- /dev/null +++ b/src/util.cpp @@ -0,0 +1,1081 @@ +//----------------------------------------------------------------------------- +// Utility functions, mostly various kinds of vector math (working on real +// numbers, not working on quantities in the symbolic algebra system). +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include +#include +#include +#include + +#include "solvespace.h" + +namespace SolveSpace { + +void AssertFailure(const char *file, unsigned line, const char *function, + const char *condition, const char *message) { + std::string formattedMsg; + formattedMsg += ssprintf("File %s, line %u, function %s:\n", file, line, function); + formattedMsg += ssprintf("Assertion failed: %s.\n", condition); + formattedMsg += ssprintf("Message: %s.\n", message); + Platform::FatalError(formattedMsg); +} + +std::string ssprintf(const char *fmt, ...) +{ + va_list va; + + va_start(va, fmt); + int size = vsnprintf(NULL, 0, fmt, va); + ssassert(size >= 0, "vsnprintf could not encode string"); + va_end(va); + + std::string result; + result.resize(size + 1); + + va_start(va, fmt); + vsnprintf(&result[0], size + 1, fmt, va); + va_end(va); + + result.resize(size); + return result; +} + +char32_t utf8_iterator::operator*() +{ + const uint8_t *it = (const uint8_t*) this->p; + char32_t result = *it; + + if((result & 0x80) != 0) { + unsigned int mask = 0x40; + + do { + result <<= 6; + unsigned int c = (*++it); + mask <<= 5; + result += c - 0x80; + } while((result & mask) != 0); + + result &= mask - 1; + } + + this->n = (const char*) (it + 1); + return result; +} + +int64_t GetMilliseconds() +{ + auto timestamp = std::chrono::steady_clock::now().time_since_epoch(); + return std::chrono::duration_cast(timestamp).count(); +} + +void MakeMatrix(double *mat, + double a11, double a12, double a13, double a14, + double a21, double a22, double a23, double a24, + double a31, double a32, double a33, double a34, + double a41, double a42, double a43, double a44) +{ + mat[ 0] = a11; + mat[ 1] = a21; + mat[ 2] = a31; + mat[ 3] = a41; + mat[ 4] = a12; + mat[ 5] = a22; + mat[ 6] = a32; + mat[ 7] = a42; + mat[ 8] = a13; + mat[ 9] = a23; + mat[10] = a33; + mat[11] = a43; + mat[12] = a14; + mat[13] = a24; + mat[14] = a34; + mat[15] = a44; +} + +void MultMatrix(double *mata, double *matb, double *matr) { + for(int i = 0; i < 4; i++) { + for(int j = 0; j < 4; j++) { + double s = 0.0; + for(int k = 0; k < 4; k++) { + s += mata[k * 4 + j] * matb[i * 4 + k]; + } + matr[i * 4 + j] = s; + } + } +} + +//----------------------------------------------------------------------------- +// Format the string for our message box appropriately, and then display +// that string. +//----------------------------------------------------------------------------- +static void MessageBox(const char *fmt, va_list va, bool error, + std::function onDismiss = std::function()) +{ +#ifndef LIBRARY + va_list va_size; + va_copy(va_size, va); + int size = vsnprintf(NULL, 0, fmt, va_size); + ssassert(size >= 0, "vsnprintf could not encode string"); + va_end(va_size); + + std::string text; + text.resize(size); + + vsnprintf(&text[0], size + 1, fmt, va); + + // Split message text using a heuristic for better presentation. + size_t separatorAt = 0; + while(separatorAt != std::string::npos) { + size_t dotAt = text.find('.', separatorAt + 1), + colonAt = text.find(':', separatorAt + 1); + separatorAt = min(dotAt, colonAt); + if(separatorAt == std::string::npos || + (separatorAt + 1 < text.size() && isspace(text[separatorAt + 1]))) { + break; + } + } + std::string message = text; + std::string description; + if(separatorAt != std::string::npos) { + message = text.substr(0, separatorAt + 1); + if(separatorAt + 1 < text.size()) { + description = text.substr(separatorAt + 1); + } + } + + if(description.length() > 0) { + std::string::iterator it = description.begin(); + while(isspace(*it)) it++; + description = description.substr(it - description.begin()); + } + + Platform::MessageDialogRef dialog = CreateMessageDialog(SS.GW.window); + if (!dialog) { + if (error) { + fprintf(stderr, "Error: %s\n", message.c_str()); + } else { + fprintf(stderr, "Message: %s\n", message.c_str()); + } + if(onDismiss) { + onDismiss(); + } + return; + } + using Platform::MessageDialog; + if(error) { + dialog->SetType(MessageDialog::Type::ERROR); + } else { + dialog->SetType(MessageDialog::Type::INFORMATION); + } + dialog->SetTitle(error ? C_("title", "Error") : C_("title", "Message")); + dialog->SetMessage(message); + if(!description.empty()) { + dialog->SetDescription(description); + } + dialog->AddButton(C_("button", "&OK"), MessageDialog::Response::OK, + /*isDefault=*/true); + + dialog->onResponse = [=](MessageDialog::Response _response) { + if(onDismiss) { + onDismiss(); + } + }; + dialog->ShowModal(); +#endif +} +void Error(const char *fmt, ...) +{ + va_list f; + va_start(f, fmt); + MessageBox(fmt, f, /*error=*/true); + va_end(f); +} +void Message(const char *fmt, ...) +{ + va_list f; + va_start(f, fmt); + MessageBox(fmt, f, /*error=*/false); + va_end(f); +} +void MessageAndRun(std::function onDismiss, const char *fmt, ...) +{ + va_list f; + va_start(f, fmt); + MessageBox(fmt, f, /*error=*/false, onDismiss); + va_end(f); +} + +//----------------------------------------------------------------------------- +// Solve a mostly banded matrix. In a given row, there are LEFT_OF_DIAG +// elements to the left of the diagonal element, and RIGHT_OF_DIAG elements to +// the right (so that the total band width is LEFT_OF_DIAG + RIGHT_OF_DIAG + 1). +// There also may be elements in the last two columns of any row. We solve +// without pivoting. +//----------------------------------------------------------------------------- +void BandedMatrix::Solve() { + int i, ip, j, jp; + double temp; + + // Reduce the matrix to upper triangular form. + for(i = 0; i < n; i++) { + for(ip = i+1; ip < n && ip <= (i + LEFT_OF_DIAG); ip++) { + temp = A[ip][i]/A[i][i]; + + for(jp = i; jp < (n - 2) && jp <= (i + RIGHT_OF_DIAG); jp++) { + A[ip][jp] -= temp*(A[i][jp]); + } + A[ip][n-2] -= temp*(A[i][n-2]); + A[ip][n-1] -= temp*(A[i][n-1]); + + B[ip] -= temp*B[i]; + } + } + + // And back-substitute. + for(i = n - 1; i >= 0; i--) { + temp = B[i]; + + if(i < n-1) temp -= X[n-1]*A[i][n-1]; + if(i < n-2) temp -= X[n-2]*A[i][n-2]; + + for(j = min(n - 3, i + RIGHT_OF_DIAG); j > i; j--) { + temp -= X[j]*A[i][j]; + } + X[i] = temp / A[i][i]; + } +} + +const Quaternion Quaternion::IDENTITY = { 1, 0, 0, 0 }; + +Quaternion Quaternion::From(double w, double vx, double vy, double vz) { + Quaternion q; + q.w = w; + q.vx = vx; + q.vy = vy; + q.vz = vz; + return q; +} + +Quaternion Quaternion::From(hParam w, hParam vx, hParam vy, hParam vz) { + Quaternion q; + q.w = SK.GetParam(w )->val; + q.vx = SK.GetParam(vx)->val; + q.vy = SK.GetParam(vy)->val; + q.vz = SK.GetParam(vz)->val; + return q; +} + +Quaternion Quaternion::From(Vector axis, double dtheta) { + Quaternion q; + double c = cos(dtheta / 2), s = sin(dtheta / 2); + axis = axis.WithMagnitude(s); + q.w = c; + q.vx = axis.x; + q.vy = axis.y; + q.vz = axis.z; + return q; +} + +Quaternion Quaternion::From(Vector u, Vector v) +{ + Vector n = u.Cross(v); + + Quaternion q; + double s, tr = 1 + u.x + v.y + n.z; + if(tr > 1e-4) { + s = 2*sqrt(tr); + q.w = s/4; + q.vx = (v.z - n.y)/s; + q.vy = (n.x - u.z)/s; + q.vz = (u.y - v.x)/s; + } else { + if(u.x > v.y && u.x > n.z) { + s = 2*sqrt(1 + u.x - v.y - n.z); + q.w = (v.z - n.y)/s; + q.vx = s/4; + q.vy = (u.y + v.x)/s; + q.vz = (n.x + u.z)/s; + } else if(v.y > n.z) { + s = 2*sqrt(1 - u.x + v.y - n.z); + q.w = (n.x - u.z)/s; + q.vx = (u.y + v.x)/s; + q.vy = s/4; + q.vz = (v.z + n.y)/s; + } else { + s = 2*sqrt(1 - u.x - v.y + n.z); + q.w = (u.y - v.x)/s; + q.vx = (n.x + u.z)/s; + q.vy = (v.z + n.y)/s; + q.vz = s/4; + } + } + + return q.WithMagnitude(1); +} + +Quaternion Quaternion::Plus(Quaternion b) const { + Quaternion q; + q.w = w + b.w; + q.vx = vx + b.vx; + q.vy = vy + b.vy; + q.vz = vz + b.vz; + return q; +} + +Quaternion Quaternion::Minus(Quaternion b) const { + Quaternion q; + q.w = w - b.w; + q.vx = vx - b.vx; + q.vy = vy - b.vy; + q.vz = vz - b.vz; + return q; +} + +Quaternion Quaternion::ScaledBy(double s) const { + Quaternion q; + q.w = w*s; + q.vx = vx*s; + q.vy = vy*s; + q.vz = vz*s; + return q; +} + +double Quaternion::Magnitude() const { + return sqrt(w*w + vx*vx + vy*vy + vz*vz); +} + +Quaternion Quaternion::WithMagnitude(double s) const { + return ScaledBy(s/Magnitude()); +} + +Vector Quaternion::RotationU() const { + Vector v; + v.x = w*w + vx*vx - vy*vy - vz*vz; + v.y = 2*w *vz + 2*vx*vy; + v.z = 2*vx*vz - 2*w *vy; + return v; +} + +Vector Quaternion::RotationV() const { + Vector v; + v.x = 2*vx*vy - 2*w*vz; + v.y = w*w - vx*vx + vy*vy - vz*vz; + v.z = 2*w*vx + 2*vy*vz; + return v; +} + +Vector Quaternion::RotationN() const { + Vector v; + v.x = 2*w*vy + 2*vx*vz; + v.y = 2*vy*vz - 2*w*vx; + v.z = w*w - vx*vx - vy*vy + vz*vz; + return v; +} + +Vector Quaternion::Rotate(Vector p) const { + // Express the point in the new basis + return (RotationU().ScaledBy(p.x)).Plus( + RotationV().ScaledBy(p.y)).Plus( + RotationN().ScaledBy(p.z)); +} + +Quaternion Quaternion::Inverse() const { + Quaternion r; + r.w = w; + r.vx = -vx; + r.vy = -vy; + r.vz = -vz; + return r.WithMagnitude(1); // not that the normalize should be reqd +} + +Quaternion Quaternion::ToThe(double p) const { + // Avoid division by zero, or arccos of something not in its domain + if(w >= (1 - 1e-6)) { + return From(1, 0, 0, 0); + } else if(w <= (-1 + 1e-6)) { + return From(-1, 0, 0, 0); + } + + Quaternion r; + Vector axis = Vector::From(vx, vy, vz); + double theta = acos(w); // okay, since magnitude is 1, so -1 <= w <= 1 + theta *= p; + r.w = cos(theta); + axis = axis.WithMagnitude(sin(theta)); + r.vx = axis.x; + r.vy = axis.y; + r.vz = axis.z; + return r; +} + +Quaternion Quaternion::Times(Quaternion b) const { + double sa = w, sb = b.w; + Vector va = { vx, vy, vz }; + Vector vb = { b.vx, b.vy, b.vz }; + + Quaternion r; + r.w = sa*sb - va.Dot(vb); + Vector vr = vb.ScaledBy(sa).Plus( + va.ScaledBy(sb).Plus( + va.Cross(vb))); + r.vx = vr.x; + r.vy = vr.y; + r.vz = vr.z; + return r; +} + +Quaternion Quaternion::Mirror() const { + Vector u = RotationU(), + v = RotationV(); + u = u.ScaledBy(-1); + v = v.ScaledBy(-1); + return Quaternion::From(u, v); +} + + +Vector Vector::From(hParam x, hParam y, hParam z) { + Vector v; + v.x = SK.GetParam(x)->val; + v.y = SK.GetParam(y)->val; + v.z = SK.GetParam(z)->val; + return v; +} + +bool Vector::EqualsExactly(Vector v) const { + return EXACT(x == v.x && + y == v.y && + z == v.z); +} + +double Vector::DirectionCosineWith(Vector b) const { + Vector a = this->WithMagnitude(1); + b = b.WithMagnitude(1); + return a.Dot(b); +} + +Vector Vector::Normal(int which) const { + Vector n; + + // Arbitrarily choose one vector that's normal to us, pivoting + // appropriately. + double xa = fabs(x), ya = fabs(y), za = fabs(z); + if(this->Equals(Vector::From(0, 0, 1))) { + // Make DXFs exported in the XY plane work nicely... + n = Vector::From(1, 0, 0); + } else if(xa < ya && xa < za) { + n.x = 0; + n.y = z; + n.z = -y; + } else if(ya < za) { + n.x = -z; + n.y = 0; + n.z = x; + } else { + n.x = y; + n.y = -x; + n.z = 0; + } + + if(which == 0) { + // That's the vector we return. + } else if(which == 1) { + n = this->Cross(n); + } else ssassert(false, "Unexpected vector normal index"); + + n = n.WithMagnitude(1); + + return n; +} + +Vector Vector::RotatedAbout(Vector orig, Vector axis, double theta) const { + Vector r = this->Minus(orig); + r = r.RotatedAbout(axis, theta); + return r.Plus(orig); +} + +Vector Vector::RotatedAbout(Vector axis, double theta) const { + double c = cos(theta); + double s = sin(theta); + + axis = axis.WithMagnitude(1); + + Vector r; + + r.x = (x)*(c + (1 - c)*(axis.x)*(axis.x)) + + (y)*((1 - c)*(axis.x)*(axis.y) - s*(axis.z)) + + (z)*((1 - c)*(axis.x)*(axis.z) + s*(axis.y)); + + r.y = (x)*((1 - c)*(axis.y)*(axis.x) + s*(axis.z)) + + (y)*(c + (1 - c)*(axis.y)*(axis.y)) + + (z)*((1 - c)*(axis.y)*(axis.z) - s*(axis.x)); + + r.z = (x)*((1 - c)*(axis.z)*(axis.x) - s*(axis.y)) + + (y)*((1 - c)*(axis.z)*(axis.y) + s*(axis.x)) + + (z)*(c + (1 - c)*(axis.z)*(axis.z)); + + return r; +} + +Vector Vector::DotInToCsys(Vector u, Vector v, Vector n) const { + Vector r = { + this->Dot(u), + this->Dot(v), + this->Dot(n) + }; + return r; +} + +Vector Vector::ScaleOutOfCsys(Vector u, Vector v, Vector n) const { + Vector r = u.ScaledBy(x).Plus( + v.ScaledBy(y).Plus( + n.ScaledBy(z))); + return r; +} + +Vector Vector::InPerspective(Vector u, Vector v, Vector n, + Vector origin, double cameraTan) const +{ + Vector r = this->Minus(origin); + r = r.DotInToCsys(u, v, n); + // yes, minus; we are assuming a csys where u cross v equals n, backwards + // from the display stuff + double w = (1 - r.z*cameraTan); + r = r.ScaledBy(1/w); + + return r; +} + +double Vector::DistanceToLine(Vector p0, Vector dp) const { + double m = dp.Magnitude(); + return ((this->Minus(p0)).Cross(dp)).Magnitude() / m; +} + +double Vector::DistanceToPlane(Vector normal, Vector origin) const { + return this->Dot(normal) - origin.Dot(normal); +} + +bool Vector::OnLineSegment(Vector a, Vector b, double tol) const { + if(this->Equals(a, tol) || this->Equals(b, tol)) return true; + + Vector d = b.Minus(a); + + double m = d.MagSquared(); + double distsq = ((this->Minus(a)).Cross(d)).MagSquared() / m; + + if(distsq >= tol*tol) return false; + + double t = (this->Minus(a)).DivProjected(d); + // On-endpoint already tested + if(t < 0 || t > 1) return false; + return true; +} + +Vector Vector::ClosestPointOnLine(Vector p0, Vector dp) const { + dp = dp.WithMagnitude(1); + // this, p0, and (p0+dp) define a plane; the min distance is in + // that plane, so calculate its normal + Vector pn = (this->Minus(p0)).Cross(dp); + // The minimum distance line is in that plane, perpendicular + // to the line + Vector n = pn.Cross(dp); + + // Calculate the actual distance + double d = (dp.Cross(p0.Minus(*this))).Magnitude(); + return this->Plus(n.WithMagnitude(d)); +} + +Vector Vector::WithMagnitude(double v) const { + double m = Magnitude(); + if(EXACT(m == 0)) { + // We can do a zero vector with zero magnitude, but not any other cases. + if(fabs(v) > 1e-100) { + dbp("Vector::WithMagnitude(%g) of zero vector!", v); + } + return From(0, 0, 0); + } else { + return ScaledBy(v/m); + } +} + +Vector Vector::ProjectVectorInto(hEntity wrkpl) const { + EntityBase *w = SK.GetEntity(wrkpl); + Vector u = w->Normal()->NormalU(); + Vector v = w->Normal()->NormalV(); + + double up = this->Dot(u); + double vp = this->Dot(v); + + return (u.ScaledBy(up)).Plus(v.ScaledBy(vp)); +} + +Vector Vector::ProjectInto(hEntity wrkpl) const { + EntityBase *w = SK.GetEntity(wrkpl); + Vector p0 = w->WorkplaneGetOffset(); + + Vector f = this->Minus(p0); + + return p0.Plus(f.ProjectVectorInto(wrkpl)); +} + +Point2d Vector::Project2d(Vector u, Vector v) const { + Point2d p; + p.x = this->Dot(u); + p.y = this->Dot(v); + return p; +} + +Point2d Vector::ProjectXy() const { + Point2d p; + p.x = x; + p.y = y; + return p; +} + +Vector4 Vector::Project4d() const { + return Vector4::From(1, x, y, z); +} + +double Vector::DivProjected(Vector delta) const { + return (x*delta.x + y*delta.y + z*delta.z) + / (delta.x*delta.x + delta.y*delta.y + delta.z*delta.z); +} + +Vector Vector::ClosestOrtho() const { + double mx = fabs(x), my = fabs(y), mz = fabs(z); + + if(mx > my && mx > mz) { + return From((x > 0) ? 1 : -1, 0, 0); + } else if(my > mz) { + return From(0, (y > 0) ? 1 : -1, 0); + } else { + return From(0, 0, (z > 0) ? 1 : -1); + } +} + +Vector Vector::ClampWithin(double minv, double maxv) const { + Vector ret = *this; + + if(ret.x < minv) ret.x = minv; + if(ret.y < minv) ret.y = minv; + if(ret.z < minv) ret.z = minv; + + if(ret.x > maxv) ret.x = maxv; + if(ret.y > maxv) ret.y = maxv; + if(ret.z > maxv) ret.z = maxv; + + return ret; +} + +bool Vector::OutsideAndNotOn(Vector maxv, Vector minv) const { + return (x > maxv.x + LENGTH_EPS) || (x < minv.x - LENGTH_EPS) || + (y > maxv.y + LENGTH_EPS) || (y < minv.y - LENGTH_EPS) || + (z > maxv.z + LENGTH_EPS) || (z < minv.z - LENGTH_EPS); +} + +bool Vector::BoundingBoxesDisjoint(Vector amax, Vector amin, + Vector bmax, Vector bmin) +{ + int i; + for(i = 0; i < 3; i++) { + if(amax.Element(i) < bmin.Element(i) - LENGTH_EPS) return true; + if(amin.Element(i) > bmax.Element(i) + LENGTH_EPS) return true; + } + return false; +} + +bool Vector::BoundingBoxIntersectsLine(Vector amax, Vector amin, + Vector p0, Vector p1, bool asSegment) +{ + Vector dp = p1.Minus(p0); + double lp = dp.Magnitude(); + dp = dp.ScaledBy(1.0/lp); + + int i, a; + for(i = 0; i < 3; i++) { + int j = WRAP(i+1, 3), k = WRAP(i+2, 3); + if(lp*fabs(dp.Element(i)) < LENGTH_EPS) continue; // parallel to plane + + for(a = 0; a < 2; a++) { + double d = (a == 0) ? amax.Element(i) : amin.Element(i); + // n dot (p0 + t*dp) = d + // (n dot p0) + t * (n dot dp) = d + double t = (d - p0.Element(i)) / dp.Element(i); + Vector p = p0.Plus(dp.ScaledBy(t)); + + if(asSegment && (t < -LENGTH_EPS || t > (lp+LENGTH_EPS))) continue; + + if(p.Element(j) > amax.Element(j) + LENGTH_EPS) continue; + if(p.Element(k) > amax.Element(k) + LENGTH_EPS) continue; + + if(p.Element(j) < amin.Element(j) - LENGTH_EPS) continue; + if(p.Element(k) < amin.Element(k) - LENGTH_EPS) continue; + + return true; + } + } + + return false; +} + +Vector Vector::AtIntersectionOfPlanes(Vector n1, double d1, + Vector n2, double d2) +{ + double det = (n1.Dot(n1))*(n2.Dot(n2)) - + (n1.Dot(n2))*(n1.Dot(n2)); + double c1 = (d1*n2.Dot(n2) - d2*n1.Dot(n2))/det; + double c2 = (d2*n1.Dot(n1) - d1*n1.Dot(n2))/det; + + return (n1.ScaledBy(c1)).Plus(n2.ScaledBy(c2)); +} + +void Vector::ClosestPointBetweenLines(Vector a0, Vector da, + Vector b0, Vector db, + double *ta, double *tb) +{ + // Make a semi-orthogonal coordinate system from those directions; + // note that dna and dnb need not be perpendicular. + Vector dn = da.Cross(db); // normal to both + Vector dna = dn.Cross(da); // normal to da + Vector dnb = dn.Cross(db); // normal to db + + // At the intersection of the lines + // a0 + pa*da = b0 + pb*db (where pa, pb are scalar params) + // So dot this equation against dna and dnb to get two equations + // to solve for da and db + *tb = ((a0.Minus(b0)).Dot(dna))/(db.Dot(dna)); + *ta = -((a0.Minus(b0)).Dot(dnb))/(da.Dot(dnb)); +} + +Vector Vector::AtIntersectionOfLines(Vector a0, Vector a1, + Vector b0, Vector b1, + bool *skew, + double *parama, double *paramb) +{ + Vector da = a1.Minus(a0), db = b1.Minus(b0); + + double pa, pb; + Vector::ClosestPointBetweenLines(a0, da, b0, db, &pa, &pb); + + if(parama) *parama = pa; + if(paramb) *paramb = pb; + + // And from either of those, we get the intersection point. + Vector pi = a0.Plus(da.ScaledBy(pa)); + + if(skew) { + // Check if the intersection points on each line are actually + // coincident... + if(pi.Equals(b0.Plus(db.ScaledBy(pb)))) { + *skew = false; + } else { + *skew = true; + } + } + return pi; +} + +Vector Vector::AtIntersectionOfPlaneAndLine(Vector n, double d, + Vector p0, Vector p1, + bool *parallel) +{ + Vector dp = p1.Minus(p0); + + if(fabs(n.Dot(dp)) < LENGTH_EPS) { + if(parallel) *parallel = true; + return Vector::From(0, 0, 0); + } + + if(parallel) *parallel = false; + + // n dot (p0 + t*dp) = d + // (n dot p0) + t * (n dot dp) = d + double t = (d - n.Dot(p0)) / (n.Dot(dp)); + + return p0.Plus(dp.ScaledBy(t)); +} + +static double det2(double a1, double b1, + double a2, double b2) +{ + return (a1*b2) - (b1*a2); +} +static double det3(double a1, double b1, double c1, + double a2, double b2, double c2, + double a3, double b3, double c3) +{ + return a1*det2(b2, c2, b3, c3) - + b1*det2(a2, c2, a3, c3) + + c1*det2(a2, b2, a3, b3); +} +Vector Vector::AtIntersectionOfPlanes(Vector na, double da, + Vector nb, double db, + Vector nc, double dc, + bool *parallel) +{ + double det = det3(na.x, na.y, na.z, + nb.x, nb.y, nb.z, + nc.x, nc.y, nc.z); + if(fabs(det) < 1e-10) { // arbitrary tolerance, not so good + *parallel = true; + return Vector::From(0, 0, 0); + } + *parallel = false; + + double detx = det3(da, na.y, na.z, + db, nb.y, nb.z, + dc, nc.y, nc.z); + + double dety = det3(na.x, da, na.z, + nb.x, db, nb.z, + nc.x, dc, nc.z); + + double detz = det3(na.x, na.y, da, + nb.x, nb.y, db, + nc.x, nc.y, dc ); + + return Vector::From(detx/det, dety/det, detz/det); +} + +size_t VectorHash::operator()(const Vector &v) const { + const size_t size = (size_t)pow(std::numeric_limits::max(), 1.0 / 3.0) - 1; + const double eps = 4.0 * LENGTH_EPS; + + double x = fabs(v.x) / eps; + double y = fabs(v.y) / eps; + double z = fabs(v.y) / eps; + + size_t xs = size_t(fmod(x, (double)size)); + size_t ys = size_t(fmod(y, (double)size)); + size_t zs = size_t(fmod(z, (double)size)); + + return (zs * size + ys) * size + xs; +} + +bool VectorPred::operator()(Vector a, Vector b) const { + return a.Equals(b, LENGTH_EPS); +} + +Vector4 Vector4::From(double w, double x, double y, double z) { + Vector4 ret; + ret.w = w; + ret.x = x; + ret.y = y; + ret.z = z; + return ret; +} + +Vector4 Vector4::From(double w, Vector v) { + return Vector4::From(w, w*v.x, w*v.y, w*v.z); +} + +Vector4 Vector4::Blend(Vector4 a, Vector4 b, double t) { + return (a.ScaledBy(1 - t)).Plus(b.ScaledBy(t)); +} + +Vector4 Vector4::Plus(Vector4 b) const { + return Vector4::From(w + b.w, x + b.x, y + b.y, z + b.z); +} + +Vector4 Vector4::Minus(Vector4 b) const { + return Vector4::From(w - b.w, x - b.x, y - b.y, z - b.z); +} + +Vector4 Vector4::ScaledBy(double s) const { + return Vector4::From(w*s, x*s, y*s, z*s); +} + +Vector Vector4::PerspectiveProject() const { + return Vector::From(x / w, y / w, z / w); +} + +Point2d Point2d::From(double x, double y) { + return { x, y }; +} + +Point2d Point2d::FromPolar(double r, double a) { + return { r * cos(a), r * sin(a) }; +} + +double Point2d::Angle() const { + double a = atan2(y, x); + return M_PI + remainder(a - M_PI, 2 * M_PI); +} + +double Point2d::AngleTo(const Point2d &p) const { + return p.Minus(*this).Angle(); +} + +Point2d Point2d::Plus(const Point2d &b) const { + return { x + b.x, y + b.y }; +} + +Point2d Point2d::Minus(const Point2d &b) const { + return { x - b.x, y - b.y }; +} + +Point2d Point2d::ScaledBy(double s) const { + return { x * s, y * s }; +} + +double Point2d::DivProjected(Point2d delta) const { + return (x*delta.x + y*delta.y) / (delta.x*delta.x + delta.y*delta.y); +} + +double Point2d::MagSquared() const { + return x*x + y*y; +} + +double Point2d::Magnitude() const { + return sqrt(x*x + y*y); +} + +Point2d Point2d::WithMagnitude(double v) const { + double m = Magnitude(); + if(m < 1e-20) { + dbp("!!! WithMagnitude() of zero vector"); + return { v, 0 }; + } + return { x * v / m, y * v / m }; +} + +double Point2d::DistanceTo(const Point2d &p) const { + double dx = x - p.x; + double dy = y - p.y; + return sqrt(dx*dx + dy*dy); +} + +double Point2d::Dot(Point2d p) const { + return x*p.x + y*p.y; +} + +double Point2d::DistanceToLine(const Point2d &p0, const Point2d &dp, bool asSegment) const { + double m = dp.x*dp.x + dp.y*dp.y; + if(m < LENGTH_EPS*LENGTH_EPS) return VERY_POSITIVE; + + // Let our line be p = p0 + t*dp, for a scalar t from 0 to 1 + double t = (dp.x*(x - p0.x) + dp.y*(y - p0.y))/m; + + if(asSegment) { + if(t < 0.0) return DistanceTo(p0); + if(t > 1.0) return DistanceTo(p0.Plus(dp)); + } + Point2d closest = p0.Plus(dp.ScaledBy(t)); + return DistanceTo(closest); +} + +double Point2d::DistanceToLineSigned(const Point2d &p0, const Point2d &dp, bool asSegment) const { + double m = dp.x*dp.x + dp.y*dp.y; + if(m < LENGTH_EPS*LENGTH_EPS) return VERY_POSITIVE; + + Point2d n = dp.Normal().WithMagnitude(1.0); + double dist = n.Dot(*this) - n.Dot(p0); + if(asSegment) { + // Let our line be p = p0 + t*dp, for a scalar t from 0 to 1 + double t = (dp.x*(x - p0.x) + dp.y*(y - p0.y))/m; + double sign = (dist > 0.0) ? 1.0 : -1.0; + if(t < 0.0) return DistanceTo(p0) * sign; + if(t > 1.0) return DistanceTo(p0.Plus(dp)) * sign; + } + + return dist; +} + +Point2d Point2d::Normal() const { + return { y, -x }; +} + +bool Point2d::Equals(Point2d v, double tol) const { + double dx = v.x - x; if(dx < -tol || dx > tol) return false; + double dy = v.y - y; if(dy < -tol || dy > tol) return false; + + return (this->Minus(v)).MagSquared() < tol*tol; +} + +BBox BBox::From(const Vector &p0, const Vector &p1) { + BBox bbox; + bbox.minp.x = min(p0.x, p1.x); + bbox.minp.y = min(p0.y, p1.y); + bbox.minp.z = min(p0.z, p1.z); + + bbox.maxp.x = max(p0.x, p1.x); + bbox.maxp.y = max(p0.y, p1.y); + bbox.maxp.z = max(p0.z, p1.z); + return bbox; +} + +Vector BBox::GetOrigin() const { return minp.Plus(maxp.Minus(minp).ScaledBy(0.5)); } +Vector BBox::GetExtents() const { return maxp.Minus(minp).ScaledBy(0.5); } + +void BBox::Include(const Vector &v, double r) { + minp.x = min(minp.x, v.x - r); + minp.y = min(minp.y, v.y - r); + minp.z = min(minp.z, v.z - r); + + maxp.x = max(maxp.x, v.x + r); + maxp.y = max(maxp.y, v.y + r); + maxp.z = max(maxp.z, v.z + r); +} + +bool BBox::Overlaps(const BBox &b1) const { + Vector t = b1.GetOrigin().Minus(GetOrigin()); + Vector e = b1.GetExtents().Plus(GetExtents()); + + return fabs(t.x) < e.x && fabs(t.y) < e.y && fabs(t.z) < e.z; +} + +bool BBox::Contains(const Point2d &p, double r) const { + return p.x >= (minp.x - r) && + p.y >= (minp.y - r) && + p.x <= (maxp.x + r) && + p.y <= (maxp.y + r); +} + +const std::vector& StipplePatternDashes(StipplePattern pattern) { + static bool initialized; + static std::vector dashes[(size_t)StipplePattern::LAST + 1]; + if(!initialized) { + // Inkscape ignores all elements that are exactly zero instead of drawing + // them as dots, so set those to 1e-6. + dashes[(size_t)StipplePattern::CONTINUOUS] = + {}; + dashes[(size_t)StipplePattern::SHORT_DASH] = + { 1.0, 2.0 }; + dashes[(size_t)StipplePattern::DASH] = + { 1.0, 1.0 }; + dashes[(size_t)StipplePattern::DASH_DOT] = + { 1.0, 0.5, 1e-6, 0.5 }; + dashes[(size_t)StipplePattern::DASH_DOT_DOT] = + { 1.0, 0.5, 1e-6, 0.5, 1e-6, 0.5 }; + dashes[(size_t)StipplePattern::DOT] = + { 1e-6, 0.5 }; + dashes[(size_t)StipplePattern::LONG_DASH] = + { 2.0, 0.5 }; + dashes[(size_t)StipplePattern::FREEHAND] = + { 1.0, 2.0 }; + dashes[(size_t)StipplePattern::ZIGZAG] = + { 1.0, 2.0 }; + } + + return dashes[(size_t)pattern]; +} + +double StipplePatternLength(StipplePattern pattern) { + static bool initialized; + static double lengths[(size_t)StipplePattern::LAST + 1]; + if(!initialized) { + for(size_t i = 0; i < (size_t)StipplePattern::LAST; i++) { + const std::vector &dashes = StipplePatternDashes((StipplePattern)i); + double length = 0.0; + for(double dash : dashes) { + length += dash; + } + lengths[i] = length; + } + } + + return lengths[(size_t)pattern]; +} + +} // namespace SolveSpace diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..585b5de --- /dev/null +++ b/src/util.h @@ -0,0 +1,105 @@ +#ifndef SOLVESPACE_UTIL_H +#define SOLVESPACE_UTIL_H + +#include +#include +#include +#include +#include +#include + +// The few floating-point equality comparisons in SolveSpace have been +// carefully considered, so we disable the -Wfloat-equal warning for them +#ifdef __clang__ +# define EXACT(expr) \ + (_Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wfloat-equal\"") \ + (expr) \ + _Pragma("clang diagnostic pop")) +#else +# define EXACT(expr) (expr) +#endif + +// Debugging functions +#if defined(__GNUC__) +#define ssassert(condition, message) \ + do { \ + if(__builtin_expect((condition), true) == false) { \ + SolveSpace::AssertFailure(__FILE__, __LINE__, __func__, #condition, message); \ + __builtin_unreachable(); \ + } \ + } while(0) +#else +#define ssassert(condition, message) \ + do { \ + if((condition) == false) { \ + SolveSpace::AssertFailure(__FILE__, __LINE__, __func__, #condition, message); \ + std::abort(); \ + } \ + } while(0) +#endif + +#define CO(v) (v).x, (v).y, (v).z + +#define dbp SolveSpace::Platform::DebugPrint + +namespace SolveSpace { + +namespace Platform { + +// Debug print function. +void DebugPrint(const char *fmt, ...); + +} // namespace Platform + +[[noreturn]] +void AssertFailure(const char *file, unsigned line, const char *function, + const char *condition, const char *message); + +#if defined(__GNUC__) +__attribute__((__format__ (__printf__, 1, 2))) +#endif +std::string ssprintf(const char *fmt, ...); + +inline bool IsReasonable(double x) { + return std::isnan(x) || x > 1e11 || x < -1e11; +} + +inline int WRAP(int v, int n) { + // Clamp it to the range [0, n) + while(v >= n) v -= n; + while(v < 0) v += n; + return v; +} +inline double WRAP_NOT_0(double v, double n) { + // Clamp it to the range (0, n] + while(v > n) v -= n; + while(v <= 0) v += n; + return v; +} +inline double WRAP_SYMMETRIC(double v, double n) { + // Clamp it to the range (-n/2, n/2] + while(v > n/2) v -= n; + while(v <= -n/2) v += n; + return v; +} + +template +inline constexpr size_t arraylen(const T (&)[N]) { + return N; +} + +void MakeMatrix(double *mat, double a11, double a12, double a13, double a14, + double a21, double a22, double a23, double a24, + double a31, double a32, double a33, double a34, + double a41, double a42, double a43, double a44); +void MultMatrix(double *mata, double *matb, double *matr); + +int64_t GetMilliseconds(); +void Message(const char *fmt, ...); +void MessageAndRun(std::function onDismiss, const char *fmt, ...); +void Error(const char *fmt, ...); + +} // namespace SolveSpace + +#endif // !SOLVESPACE_UTIL_H diff --git a/src/view.cpp b/src/view.cpp new file mode 100644 index 0000000..6260253 --- /dev/null +++ b/src/view.cpp @@ -0,0 +1,174 @@ +//----------------------------------------------------------------------------- +// The View menu, stuff to snap to certain special views of the model, and to +// display our current view of the model to the user. +// +// Copyright 2008-2013 Jonathan Westhues. +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +void TextWindow::ShowEditView() { + Printf(true, "%Ft3D VIEW PARAMETERS%E"); + + Printf(true, "%Bd %Ftoverall scale factor%E"); + Printf(false, "%Ba %# px/%s %Fl%Ll%f[edit]%E", + SS.GW.scale * SS.MmPerUnit(), + SS.UnitName(), + &ScreenChangeViewScale); + Printf(false, "%Bd %Fl%Ll%fset to full scale%E", + &ScreenChangeViewToFullScale); + Printf(false, ""); + + Printf(false, "%Bd %Ftorigin (maps to center of screen)%E"); + Printf(false, "%Ba (%s, %s, %s) %Fl%Ll%f[edit]%E", + SS.MmToString(-SS.GW.offset.x).c_str(), + SS.MmToString(-SS.GW.offset.y).c_str(), + SS.MmToString(-SS.GW.offset.z).c_str(), + &ScreenChangeViewOrigin); + Printf(false, ""); + + Vector n = (SS.GW.projRight).Cross(SS.GW.projUp); + Printf(false, "%Bd %Ftprojection onto screen%E"); + Printf(false, "%Ba %Ftright%E (%3, %3, %3) %Fl%Ll%f[edit]%E", + CO(SS.GW.projRight), + &ScreenChangeViewProjection); + Printf(false, "%Bd %Ftup%E (%3, %3, %3)", CO(SS.GW.projUp)); + Printf(false, "%Ba %Ftout%E (%3, %3, %3)", CO(n)); + Printf(false, ""); + + Printf(false, "%Ft perspective factor (0 for parallel)%E"); + Printf(false, "%Ba %# %Fl%Ll%f%D[change]%E", + SS.cameraTangent*1000, + &ScreenChangeCameraTangent, 0); + + Printf(false, ""); + Printf(false, "%Ft light direction intensity"); + for(int i = 0; i < 2; i++) { + Printf(false, "%Bp #%d (%2,%2,%2)%Fl%D%f%Ll[c]%E " + "%2 %Fl%D%f%Ll[c]%E", + (i & 1) ? 'd' : 'a', i, + CO(SS.lightDir[i]), i, &ScreenChangeLightDirection, + SS.lightIntensity[i], i, &ScreenChangeLightIntensity); + } + Printf(false, "%Ba ambient lighting %2 %Fl%f%Ll[c]%E", + SS.ambientIntensity, &ScreenChangeLightAmbient); + + Printf(false, ""); + Printf(false, "%Ft explode distance%E"); + Printf(false, "%Ba %s %Fl%Ll%f%D[change]%E", + SS.MmToString(SS.explodeDistance).c_str(), + &ScreenChangeExplodeDistance, 0); + +} + +void TextWindow::ScreenChangeViewScale(int link, uint32_t v) { + SS.TW.edit.meaning = Edit::VIEW_SCALE; + SS.TW.ShowEditControl(3, ssprintf("%.3f", SS.GW.scale * SS.MmPerUnit())); +} + +void TextWindow::ScreenChangeViewToFullScale(int link, uint32_t v) { + SS.GW.scale = SS.GW.window->GetPixelDensity() / 25.4; +} + +void TextWindow::ScreenChangeViewOrigin(int link, uint32_t v) { + std::string edit_value = + ssprintf("%s, %s, %s", + SS.MmToString(-SS.GW.offset.x, true).c_str(), + SS.MmToString(-SS.GW.offset.y, true).c_str(), + SS.MmToString(-SS.GW.offset.z, true).c_str()); + + SS.TW.edit.meaning = Edit::VIEW_ORIGIN; + SS.TW.ShowEditControl(3, edit_value); +} + +void TextWindow::ScreenChangeViewProjection(int link, uint32_t v) { + std::string edit_value = + ssprintf("%.3f, %.3f, %.3f", CO(SS.GW.projRight)); + SS.TW.edit.meaning = Edit::VIEW_PROJ_RIGHT; + SS.TW.ShowEditControl(10, edit_value); +} + +void TextWindow::ScreenChangeLightDirection(int link, uint32_t v) { + SS.TW.ShowEditControl(8, ssprintf("%.2f, %.2f, %.2f", CO(SS.lightDir[v]))); + SS.TW.edit.meaning = Edit::LIGHT_DIRECTION; + SS.TW.edit.i = v; +} + +void TextWindow::ScreenChangeLightIntensity(int link, uint32_t v) { + SS.TW.ShowEditControl(31, ssprintf("%.2f", SS.lightIntensity[v])); + SS.TW.edit.meaning = Edit::LIGHT_INTENSITY; + SS.TW.edit.i = v; +} + +void TextWindow::ScreenChangeLightAmbient(int link, uint32_t v) { + SS.TW.ShowEditControl(31, ssprintf("%.2f", SS.ambientIntensity)); + SS.TW.edit.meaning = Edit::LIGHT_AMBIENT; + SS.TW.edit.i = 0; +} + +void TextWindow::ScreenChangeCameraTangent(int link, uint32_t v) { + SS.TW.ShowEditControl(3, ssprintf("%.3f", 1000*SS.cameraTangent)); + SS.TW.edit.meaning = Edit::CAMERA_TANGENT; +} + +void TextWindow::ScreenChangeExplodeDistance(int link, uint32_t v) { + SS.TW.ShowEditControl(3, SS.MmToString(SS.explodeDistance, true)); + SS.TW.edit.meaning = Edit::EXPLODE_DISTANCE; +} + +bool TextWindow::EditControlDoneForView(const std::string &s) { + switch(edit.meaning) { + case Edit::VIEW_SCALE: { + Expr *e = Expr::From(s, /*popUpError=*/true); + if(e) { + double v = e->Eval() / SS.MmPerUnit(); + if(v > LENGTH_EPS) { + SS.GW.scale = v; + } else { + Error(_("Scale cannot be zero or negative.")); + } + } + break; + } + + case Edit::VIEW_ORIGIN: { + Vector pt; + if(sscanf(s.c_str(), "%lf, %lf, %lf", &pt.x, &pt.y, &pt.z) == 3) { + pt = pt.ScaledBy(SS.MmPerUnit()); + SS.GW.offset = pt.ScaledBy(-1); + } else { + Error(_("Bad format: specify x, y, z")); + } + break; + } + + case Edit::VIEW_PROJ_RIGHT: + case Edit::VIEW_PROJ_UP: { + Vector pt; + if(sscanf(s.c_str(), "%lf, %lf, %lf", &pt.x, &pt.y, &pt.z) != 3) { + Error(_("Bad format: specify x, y, z")); + break; + } + if(edit.meaning == Edit::VIEW_PROJ_RIGHT) { + SS.GW.projRight = pt; + SS.GW.NormalizeProjectionVectors(); + edit.meaning = Edit::VIEW_PROJ_UP; + HideEditControl(); + ShowEditControl(10, ssprintf("%.3f, %.3f, %.3f", CO(SS.GW.projUp)), + editControl.halfRow + 2); + edit.showAgain = true; + } else { + SS.GW.projUp = pt; + SS.GW.NormalizeProjectionVectors(); + } + break; + } + + default: + return false; + } + return true; +} + +} // namespace SolveSpace diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..eb6cf60 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,141 @@ +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR}) + +foreach(pkg_config_lib CAIRO) + include_directories(${${pkg_config_lib}_INCLUDE_DIRS}) + link_directories(${${pkg_config_lib}_LIBRARY_DIRS}) +endforeach() + +if(${CMAKE_HOST_SYSTEM_NAME} STREQUAL "Windows") + add_definitions(-DTEST_BUILD_ON_WINDOWS) +endif() + +# test suite + +set(testsuite_SOURCES + harness.cpp + analysis/contour_area/test.cpp + core/expr/test.cpp + core/locale/test.cpp + core/path/test.cpp + constraint/points_coincident/test.cpp + constraint/pt_pt_distance/test.cpp + constraint/pt_plane_distance/test.cpp + constraint/pt_line_distance/test.cpp + constraint/pt_face_distance/test.cpp + constraint/proj_pt_distance/test.cpp + constraint/pt_in_plane/test.cpp + constraint/pt_on_line/test.cpp + constraint/pt_on_face/test.cpp + constraint/equal_length_lines/test.cpp + constraint/length_ratio/test.cpp + constraint/eq_len_pt_line_d/test.cpp + constraint/eq_pt_ln_distances/test.cpp + constraint/equal_angle/test.cpp + constraint/equal_line_arc_len/test.cpp + constraint/length_difference/test.cpp + constraint/symmetric/test.cpp + constraint/symmetric_horiz/test.cpp + constraint/symmetric_vert/test.cpp + constraint/symmetric_line/test.cpp + constraint/at_midpoint/test.cpp + constraint/horizontal/test.cpp + constraint/vertical/test.cpp + constraint/diameter/test.cpp + constraint/pt_on_circle/test.cpp + constraint/same_orientation/test.cpp + constraint/angle/test.cpp + constraint/parallel/test.cpp + constraint/perpendicular/test.cpp + constraint/arc_line_tangent/test.cpp + constraint/cubic_line_tangent/test.cpp + constraint/curve_curve_tangent/test.cpp + constraint/equal_radius/test.cpp + constraint/where_dragged/test.cpp + constraint/comment/test.cpp + request/arc_of_circle/test.cpp + request/circle/test.cpp + request/cubic/test.cpp + request/cubic_periodic/test.cpp + request/datum_point/test.cpp + request/image/test.cpp + request/line_segment/test.cpp + request/ttf_text/test.cpp + request/workplane/test.cpp + group/link/test.cpp + group/translate_asy/test.cpp + group/translate_nd/test.cpp +) + +add_executable(solvespace-testsuite + ${testsuite_SOURCES} + $) + +target_link_libraries(solvespace-testsuite + solvespace-headless + ${COVERAGE_LIBRARY}) + +target_include_directories(solvespace-testsuite + PRIVATE + ${EIGEN3_INCLUDE_DIRS}) +add_dependencies(solvespace-testsuite + resources) + +add_custom_target(test_solvespace + COMMAND $ + COMMENT "Testing SolveSpace" + VERBATIM) + +# coverage reports + +if(ENABLE_COVERAGE) + set(LCOV_FLAGS -q --gcov-tool ${GCOV}) + set(LCOV_FLAGS ${LCOV_FLAGS} --rc lcov_branch_coverage=1) + set(LCOV_FLAGS ${LCOV_FLAGS} --rc "lcov_excl_line=(ssassert|switch)") + set(LCOV_FLAGS ${LCOV_FLAGS} --rc "lcov_excl_br_line=BRANCH_ALWAYS_TAKEN") + set(LCOV_COLLECT -c -b ${CMAKE_SOURCE_DIR}/src -d ${CMAKE_BINARY_DIR}/src --no-external) + + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/coverage_base.info + COMMAND ${LCOV} ${LCOV_FLAGS} ${LCOV_COLLECT} + -o ${CMAKE_BINARY_DIR}/coverage_base.info -i + DEPENDS solvespace-testsuite + COMMENT "Importing baseline coverage data" + VERBATIM) + + add_custom_target(coverage_solvespace ALL + COMMAND ${LCOV} ${LCOV_FLAGS} ${LCOV_COLLECT} + -o ${CMAKE_BINARY_DIR}/coverage_test.info + COMMAND ${LCOV} ${LCOV_FLAGS} + -o ${CMAKE_BINARY_DIR}/coverage_full.info + -a ${CMAKE_BINARY_DIR}/coverage_base.info + -a ${CMAKE_BINARY_DIR}/coverage_test.info + COMMAND ${LCOV} ${LCOV_FLAGS} --summary + ${CMAKE_BINARY_DIR}/coverage_full.info + COMMAND ${GENHTML} -q --branch-coverage --demangle-cpp --legend + ${CMAKE_BINARY_DIR}/coverage_full.info + -o ${CMAKE_BINARY_DIR}/coverage/ + -t "SolveSpace testbench" + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/coverage_base.info + DEPENDS test_solvespace + COMMENT "Generating coverage report" + VERBATIM) +endif() + +# debug runner + +set(debugtool_SOURCES + debugtool.cpp +) + +add_executable(solvespace-debugtool + ${debugtool_SOURCES} + $) + +target_link_libraries(solvespace-debugtool + solvespace-core + solvespace-headless) + +add_dependencies(solvespace-debugtool + resources) diff --git a/test/Gentium-R.ttf b/test/Gentium-R.ttf new file mode 100644 index 0000000000000000000000000000000000000000..3b44ad16d4b7ba32359857f73142a01d11687ec4 GIT binary patch literal 362644 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⁡v!y+nU!5NkXXIK`TVOem7WvQHD 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

$Js7$j;=G7{{ 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

}IQ3yn`RY-S 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

!a-aJ<2Y?`Gu%^5ytw(DF5Qml)dyxWiMZ?>=m%R z6233RSY0+y+4y0~CJ>J#+I96-WtYFA>`#@l*KSev`nQz55&E0&R`xdduoAiHPT1bL zLfN~PDSOX4W!KD7_JMA|`^r9q^45N-?79O1)c?o^Wq-a`*~ifDzeJtCg8o}Fj1XVmrTZOXnr0=Qq< zH&Fi;_^|aBWp^B=?5-+hTT7Mw%V1^SL2TZ2l>O^u?1L)r9`&d?K&@7;yu*LpFfAd8 zVxE!rVZAKS4jy>pq791a-2BBuZ z%!5=Y|(5QQZ;~qHvMR42`$G^|7 zt6$hqJ-ezYe0=$U@bIBSPdah<&|$*{g{Kr2g$rw&DjRCcnyTw+%WA^MS2Z=&j~z0k zvAm(WzNv9=V|C5ox`x?91`JLgUs>B!-8{FXa&~h~SpzQZa^=`?aDFT*7&a_;Gp(|r z5e0{b4IX;ZDLipXcv3+T-VX~mRMu3MHC6_e!ArQhFdwUif|U9lr=a#y|fB$(P~QFtfu*84VB_&O?7!?Z6mI4 zuC1tS2sc$#($`7#m9@Tck#8_4+!4lMgTpK=xK@fOudA;{Au}s$>gEq(wGzXc#=3CX zyt3+=vY9oNetXNp1(7quWldwl-_yzHne_DZ6aV-B(uGiyCdZ1y1(S+P!$pPpvEnJQ z&dm>>7#=<{Tu?c)p}DMKp>T23|E&tsCzr$`6Z48XD7 zS>Dt*h($El*Vi>Pu{NG9Z>Z!z3RljWg%fN_S-Ehzx|}^zQ(Zf|xf)fMqwu+Nn`^6^ zsw+{LkOgf*>GPx=W#L&3m3V=+PS`N30pvn6FAP`LhUZsRmshbhjp4av3(@i6#ws{f z;rIPqDFkm|Szp$G!O_rIRbB5hpbl|qXe1#KAB@AY8jz-u4(w;6cmh?O6xcV>jySA<=+*nrwiY^r8 z+{Vh9d6kVqWIw3zzq+=(26f1VwF}V=4b}7fpo`NuUtU%#&6$ajQX>tioIA6!qC$?k zwgT-Mg0#TeI7X$o#?+V#!i!K>l{K-ZhSU(Q<>*bdR@|1>ezPH++RPAioJSk0>gIz! z4Q#BqjP?d8E;YiXG3oDX zoc1dZm(9Y2NH39DgL$*D35h~N3SXr3NtjNnWxfnDbLX-pK9V_*CS>YEFLw#04E*}K zT9kz>-5BmXhQ2?3Qs>0f;QNj`+3OvnW-d~W@VBgjW2&ic5PG_%vI#606qfPPJQF#n zsaX`^6HiR#3OU6DQ->)7YVy@jrkxs{O0VAt0eWR&xV);Y7J-=!o;A!ZlLXixYRQr* z26hdGFg9FUIX_%kJFmK-u2y0!&PJM>s_Gg#ht+qnv3fQ#8WMn1Qz=In)U%Nu=gM(Y zWqDO?bvgQFenYkNBC7WXTs_E(&X`|Shxpg-&sqVUyR-?_PmYyLESxe010j4|IDb-c zG#C_3PEPA>6)aQYxn1D6ot9wd#VK^WL`^+ z?^)n)-F#^*qE&%Os;;(iYZ+=PSR{vs7vOXO7N6<2?5L^98vj^Kmd?RF+}h zW!DLX(3$?ki4_7?2cq}a*ht4%w=pXv0tllMun5|HNsTF-HvG zDn{(2*};(hX4O39Pbq$0>tx$?3}GRM5ge=@P)TZ4IJd3|Iia$)qOJjPlYv%&rAAXV z-bjl1<|u;cg7V6G4(GD+3v27ft|h z4`Oq&ADb=?2FXj7kZ__SIWaHDay4Kz@^lRxp4;5WbyVj`68(Th5<2eN%OO7r@}eMw zoiFt5Ij5od4PC}1a_Rr`Yp*VmK!8eRmMMwp@uxkbxq!Z@VRbxRv zE0tcQPf}SYm7Vvmv@zaMcC>Ixeo>@wVyq;+bVAI(9h)+#p!BRrNi1AAB|N!g(zL>8 zEE>*=Ou_rCLE*CsOD9a4S{jB$Nu;>+9NZs-BgN;0&nPU84hqN4#*NmLDd9;a;lhcN ziwa}V6&B|gO^p^7j}PbJy5dQ4!#A<86or;f;-x`Rg)v-L5Jp?_C!pj=9<~lj&lwah zC@d|OG7C^>Bs@7%Qd*clwJ1^&o;%HWW0EgDp$+`4>}?sks2Q>j+I1;u&pvVmR~3b2wh=G zEFTSoCBhBop+!kpQ%s4SITZ@nrK&(5Oo*}RXmbSr=LoHQ&gcU!fT@yatW{+=szyn2RKpUaq$ZR+7Jndd2sR4KSwlT^jcPDZ4ZFd( zq5-wyPtM~w9b1#*QJ$1qt(w(bRf2c3aa4m+8-jYjzt*v+x9jzy5g(-GUC&HoyQLM< z<}l6=rl+TLxmmua{OaFg| zBj49*Vd8vv-vlqi2om*Per34vO z-^EB_UAR-ry4IU&O*vXwkMjJM%*1OAH1j!DQy!ji|N}Wyq4mq2w5zj+9{CV z*9&2^T{6i?UR#NXHK50Z6AP)AEIEoS`v1;L@pLjqM-2ToQRTt2*nVCMb7!O9B{$31 z@N-X!r$5Y3nGyURm9dx_3zCb%C{MEW+#pNKd>~Acyk3KmA>&r^UYJrwtZ=iQqg3*I zsx89d8vGMB%C-J{BKBeANy*V-TaPn!Y7VrL2^-1qR4bZsww~9DWy-gXYi0hCyq%h- z!gx1}WjC;_R7=WGb}Cm(#_h-&GWu#cAIWHva^$SEd@lX3g+{cJe`G|b#-86ozwPsI z-j9cju~}@B_?WVl*!a0Z&iUEQkEYBSQolr3GF3TbM{J~5Wo{8ZN(PXAslt2lS@Kh= zeCerFOZ+o_nf0{wXOjkATMfI;IiU`|NnVq)U9+atmYOGpVSe_J7NvTjYu=Lfr256T z5+3=jZbs?SE{UJSxEaTOW)sa^#9H3Z3UnQQ`F{HSA}g7hxKg<2*Cl6#c@iC&xx;Lu z^kwS3-%|g)Sceg5@mN|R%#?Cw@_OHLF56kdIY{E+XKA^v!!v2IM7JE*`+lTqtYt~U z18J*dV&RR?_Ec~C91<={pZn1fuVe<36^I`rSy9Nj3j9my{!=V`9t?p`GE;T<)-k_| zg~UP5%|g36=DrkfdipY1zxq}7Y zb-f?QDtI#=ZIc)`kX?RcQ!Vl1E3NNX-HC^t=k-)%I!1y-U?E3rDtmNgdgt~^*%zU_ zYQ{(+;^#Esi}XbYZ=_Ti5pso$X@7)vZi&SGyH+BSW$SR2Fg)d18QND5ucUYUa#MbY zZ8=)h(Uy)J>5m-gAE{l&iO>74?etsjpAodPkWu3VvYd49;EI{&4%9B)^gmJ<`e_oU9AiXGd-|o@Qqd5GXcRv$Qj*NJz>v)tR zbKZX!eSdVxoZz!a=0J&iYSr&Y)z3hUC{uEHiX(o1rgC`ayy0i_I@mYh3O~oBJd_+G zSwY?lU%Rf$I**L5ecdq+cI5J`|7vaheSMK)Rp;^E$gxq5)^*O<(l06RrG?Vd66+3* z`77G4_oOl#NULQ`_+!WKo6hZ@gi_=#R-)(MhfZV3sr$U|YkP`Esa1%-zDV60`m8Dd zEBq1rO?IZ%0J8d!yD_m6AN_uo`AXXU%~|Px;bCWAC!)5lbK!sIYpS2VD_8nVl{2Y~ zEGr?YC6xo}m^;Nn7%sDxoDmkLW|U^|(O(_O?47cZ6|}6O#a`BNG86bCR{Gu7%DP5I zp49GJr`qcGK<8ePRZQ1tceVK;Civq<#-@xxsmEuHWI8`1%SiP5(f7>nuha^&BO?C2 z-hX08D(gy4uEZ=PE6-ZYM3OPZhZLVX_ilu-lXx`XZuPsfUn)A%8^Z9}EZ?8qqz2z3 zS^4-nfBv40cQPYOt-fBylte++uQDsvpuYJyC#zY1T>25{JnzXWRWh-(-(Qi+j3who z*#Awg`w>X>-}lF=BLZ@#R)YJOLd*_w|0eT9SRDuC^Empg`C~F$G#9OIWHkJi`B~Lp zx5|B+^tc}ZX=iH1)UgitS>$s4&`+ zPWmmqrdS_3Z()+};=;2-<_mug|hI#90aSSS0oG7ElJ9uiw&kw0_Dn6Bw^b!tx` zWh1j-N7<>fzNh|u&;OaXG7HSye?OPmf1JCWLF7{@fWI!4an|`B#BZ1M*Q`$WV#DeE|6cEZ-*^5zA!F(L?n}Opal$m8P5;Y1 zh(z~4-G#{gy^P(?qt9ndYJ^H}rug9RHTZX3K8L%mIQ$i~bjd%`TB%W9{k91wD>!CS zca*96QXcrNn8}i56(B6?@Jiw=edPPz_5QpLXQXZZS~68n*EoyEG8g*mZW*sKZfP)TF5N#q{4)I2 zfb7?Gtfy;({z=`Nrsj^W>ja;X!tWGI{MChIYkyB6#TK#jW0{)Ozd4trdb{(z=m_?= zv{zV_Dl--FuItGk^1S1!&i65$S8*K{^0`kc+8sUq?Yjzp2hX3AQ+M-zzV*G4TbM8?d*i!SQ)2ID%pbZ(@=F#bNMHE8kXpX$UQ^g0Juaif&y4@geumVLinHwR z`!#iC;`i0qx%?EvI^REh-`wfn>HPm;pX~cN(ZQn9{dYOVDzbk+OjffUS+r}U{GOIE zCG)S$1-0lIKS!kcY97wY9<4A*a!p74J7=D6u73O#ZYoPl$=}?q^=JH!*2_KE6x@#$ zq3wmpA9Ba$w|xSRBX*vTh&=RhN6hxuOKodNq|lqKwqsk4L6#N;kgEK4Azt&{o5 zpVTRKa)tOY74PMEJalXlCl<$hH9Oj=qBeMhanXQ@^d!p5&pTINS4A3peP zj-YILfR~oU}9oOolAMS^1oDDz1`uqMeG@$*}W3x0I5lRnl77 zTP;QzC43!^Ql$m{=be76K2RfXjds)AvKA2VkuE6#rX(ImFuO}GpO}jCNYs- zmbP@1BJ;o0?YBeXKP@Ovo^%lZi||iM_8Ic+c7$02Ks=_HLOL|A*QN$=rLHuIq5h=;n$o0Y=;dg!z zLBC9&-4fN5tsg^wCqw%0O!TH7cfY)EdLe$D#h7(O-H)@hP`dI3eHmBMn zoE0vmMnXqaWRyuuq$cTWc_%fb z4B^KpRkxJg;k&RyV(GU~dfAW050~p_uddI*blo%m_ER(BYY`%mnnl0;WK!32_$H!} zFF7jp^(!jmA9a=9J$SF>Ynb@1e6w4aqa44pxV#9{8qYFO{JVRlQZFI!qm| z`luuDEy*KQKYaf(td2%%?5~bd$6`Y)8y`^&Py^Ko=$;dCGc$ybI}OJ}TO-uTcwk|a z8m-1)i{upiBHpR$G?k-r**W3j()dlKDYKAJ)M%|;9s;ks*)lF)dx<>sN-}rn0-}Jm%ZBL=AI<-Z;uHL}1 zJc+HLYAi12VuGtxt1zuy#M@NaHfq8`c0O){7Gd$aSY51^s3+82>QZ%yx=dZJo>Xt* zTdzM?zfo({TdGxUQ`^-J)uP^3yVULKG4%`excZfPR;^dRRL`j^)H5os9#YS%r_}G% za`m+Oz4DZUZ_Mu1@@otqtAF4ds-LKR>aXfU^*8l*^`2^1?`uc9+S3Q<1NA}b-|8#f zO?TI6I$eFOl@95H^&wj89=fOQr8D%Qy0^~Mhv~z0AAN-Gt3Ffz(nsok`Y0XNN9!!z zUmv57)yL^!%rll7UpL{HJB>I?Oy+O6KvQ}r}` zmOfjbqtDgn>FN4>eS!Xwo}tV1OkJ)kbfuo9XX`3mt>@?qb&Z~@YtL|?2g(Uz5cpXodFoqCnNOW&>U(W~{ndX2tM->)Cg59){X zTK%wIrytRe>YwY!^e^;!{Y(A0{*``0|5`t(f1@|(-|DCI@AT99_xc(A2mP#mPCu{z zs9(^3(l6?l^hW)%-lR9{SM;CttNJzlx_(1%(QoRv^xJx?-ln(f9eStUrCW5XZqt9! zyY)MIkA7G0)qmCR>A&fA{dfJo{y=}I|Diw9AL~!_KlMKSss2p=OMkAv&|m6*>#y|J z_=y#5jIqWsuJOzP=0J0h>1Mi{G?Q*Z=3sM(>0x@BUM9mFYI>VYbC@~Y^f5=6zUD~N z&m3jK=4g{;`kQ0SvF13FZH_ks%s_L38Dvg0gUt{#)C@Dj%}Hj2IoXUfqs(YC#*8(m zm~rM*bDGI9xh7)rOumVlm?^+d*iA5n=5%w0DZ)?A6`M(BvN_X~m?@^zOf}QYS>|kW zjyczyXQrF;%?0L1W`-#&zqO zQS)>2nE8cSZ+>YWH@`AZm|vSG&2P*G^IP+j`JH*%{N6lc{$QRp&za}VAI%HqPv%AQ zlG$ipHk-_5^NRVidDXmTUN>);E#^)0mU-K3HQUT~v%~B(yG)B|HErfE_`#xg%pUWu z*=zo4-ZOtQ?fChe_ss|9L-P;wk@?tsV*Y9NnNQ7U=3nM>^M(1+{M&qGzP8G0Ypk`7 zb**O)um{?MY&YB8rrC5GvIpBkY!BO0-EVu@411{UZ8Pm*_Hf(B9%1|1BW*u>lnvXX zZIPKj2&xFvE%Hi z_B5Mgb8W=t*?b$dFuMRuYswv+5+d!{Y1Q*5c7YNy$=?Ai7ld#*jt zPPgaV3+#{V3|nSr+HzZAEA1>h+g90XJI7vVYwTQGYwK*iy~sA$M%!eY?L0f*F0c#j zBD>fwu@~D*?4|ZHd%3;BUTK%wtL!oxw+WlHSKA-k<@P7`8v9dwt-a1(Z*Q}q?jU1RUF_uB{TgZ3f2);?_4*+=Z7_UHC7 z`wP3?{?a~fe`TMrzqU`>-`EZIxArOfJNvZ#y?w_1!9HuBv(MW<+86Af?2Gm#yV1UE zH`&eh75iuVs(sDAZr`w5?3?y2`?lR`x7qD>huvv+*%sSs+w5QLZu^eiW8bxV?O*MC z_HVY`{@uQBKd>L#f7p-g$MzHZPrJ{4YCp68vY*>8?3ec6_AC3fqa5uR$2yMVI-YZY zbD(pO)6MDbq&ewM$T`?K#OdMmbb2`%&Y@0kC(}91Io#>v9O3kJj&%AtM>%2VXeZ0* z?;PVC>m27~JI6Z%oPo{>&LHPRXRtHG8R`skhC3%YBb<|+kzv|@b53&QxcbbCz?qbB=Sa zbDlHZIp4X!`H?fjDRX8zCAFwJ5^4#Gsn5msd45ywN9N=?_A_GIE_w|)9lQ1 z=HoB0FLV|;i=8FT#m*(prOsu}<<1q(mCjP_&ehJ3o#oC?oNJt)I@dba zIoCTkI4hhRotvDSom-q+o!gw-ot4hdoI9L5omI|V&fU&E&T8jgXN_~8bHDR|^PuyP zv(|aoS?4_BJnH=1dCd8Rv)=io^SJXX=LzT6&Xdk>oDI%zou{1NIZr#kcb;+n;5_R* z=REKH(Rso7lk=kUlC#ly+1cc5c3yG*?7Zr{=DhB_;cRinSeHs>$SZs#3mkMpjx*ZHgSp7S@S-TAxozVm_eq4N*tBj;o16X&1KKIc>CGv{B< z=gt?-m(IVPubi)4mKK3yT`i&+=1>1?jZL>cd$Fe9qJBq zhr1`aBixhSk?tsWv^&Nf>z?9{b5C_ob93BWH{#~G`EJyWxdrZccY<5!p6;IE7P%AM zVt0}|**()Oai_SY?o@Z0dzO2)dyadqd!9SpJ>R{+{gFGvEpunO@IOHb}w-+buV)-cdu}- zbeFnUxy#(Rn{bov)$Who->i*n)%>9MC-uc`=a}jyU~5w-Q;d|UvdBJ zzUsc_zV5!^ZgJmq-*Vq}x4PTh?d}eDr@PB-aa-Ls_b={l_Z@eS`>wm!{j2+)`!~1U z{k!|V`+@tR`w#ac_ha`H_n+=Q_fz*X_h0Vk?icQt?!Voy+^;?5Y0r4pb3E7cyaT)g zy@R}NUUx6eOZP(F!QLTW53i@!%ggW%^?G}m-eKP1ULWrWudjEc*Uvl33wuZ7FPilC zj`5E5j`Om;du6?o&l30|Rhx_5?GD;+Io`S6 zdERvIeD4D9N8Svt%$we{cx6F%s2`}kg?fuwW?)}8O#`~#vt#_Sw zy?2AR!n@JC$-CLR#kHW;R!@JX4<=y4o?cL+8_U`r8c=vhtdk=UIdJlPP zy@$PZ-Xq?l2jCYPh7OGk4PK)MMDYDed``rgGPw2R|dsWA=Ght>~gHr|J6*l2(`jsBOK8%+G3Ik8@{YrXL=zCJ_ zhpsSabG83!3QsrHbGo12QEvA+9mi>BbZSW2g75mrM`qvkCx+JZ((;KhYzrNNFaBW$A9lm5*S6@|W zW3{s_zN>z~q`>*Q!1+nO^L2blpH$UcJG-o*d2UTvb5nX<=XYL---ZUi4JDnbO>5}% z#w+pL(BQvJ@vmy+OZO=qq3hn*aZFYZ8#z39J;^KeFK*(CQz{+VBpo@`?@0V!fOO>4 zz!!WinH@RR_obOH?$ic+ZQN}ZnLf3%C+W?d-yJYD=K_x-J0_@iOi=Hbpx!Y-y<>uU#{~6`3F;ja z)H^1qcT7<4n4sP<1@0vLf&%7Ik$xK@fzOek?nqE~B&a(Q)Ex=xjs$f_g1RF?-I1W~ zNKkhqs5=sKr?SrGjym%KfAfO6^Mbnbg1Ym9y7Pj%^Mbnbg1Ym9y7Pj%^Mbnbg1Ym9 zy7L0R^MZKi2l37i>dz1A&kyR)59-ek>dz1A&kyR)59-ek>dz1A&kyR)59-ek>dz1A z&kyR42K7gS`lCVp(V+flP=7S2KN{2@4eE~u^+$vHqe1=Ap#Er3e>A8+8q^;R>W>BW z$AbD}LH)6y{#a0d>|}4eKk8@m#b-(^s67_c9t&!Z1+~Y5+G9cOvHz>9v+Hf+xWaIw zPm7i;r07L2i{1tZxHH2K0llHbeT$-K6ErE(_Kt2Wn~7|>mK7%f`p5d~`tF>E+;J|X znX@~)@0{iAJcq->CF(uuLS3i}b)hcQh4;dH;l0q;)nqlyByp2(_SMr>UpJj@{7?o@ z@lU^axZC*4^R<3I)~h#u$P{JtCQYL^X&Svr)96i_MsLzIdXuKnn>3Byq-k7@G>xl~ zrg1gWG_FRP#??sExEg61S0i=kcj$NMcj$NMcj$NMcZ>6{nkoMLp)tavj-GV%q@yPt zJ?ZF4M^6&U3FU-xLOG$F@I2vp!t;b)LNB40&`ang^fI1jJkRK7^k?j!v46(?*=|4m zo6)}+{hQIh8U35lzZw0T(Z3n}o6)}+{hQIhIr?+-VPTr1KSzI#{v7=|`g8Q>=+Dug zqd!M~j{Y3|IrwK|99X*#QMZL=jteTD}a9%Oisr^}01_xIn|M{qPp#n{hKAwz`>6*5%F zP$5Hw3>7j|$WS3eg$xxkRLEW2d%25yFL!b8Wmu45L52kx7GzkEVL?tY7ITWRpHn(_ zL;;UW|+LWv0|mzaQaLBF71&@bp0^b7h0 z{epf$zo1{x2Rs?@WWbY|@G}#BW?vylpq<{aZh#$&W7@Hb3e8lgS=dEo{(K-5;Hyv8%`0H1$9SuUqfA(XQHh%y8`uVq;gTFi;{PpqR zA6vf*uF3~rZV&(V>i+nv?aSAj!>>1wr}X0R%U9dO8~=Uzust8nv+Kbn4R-cqvv>Ub zMdQH}cOcvy`d@3md5rzLL+i%ldi>FIH$Q$kICJdhYf8r<9z20=kwK#ll-_SpNYrp(#u0wa{*pK&l zxj7$ByX)5FFS>coIpbAgXEi4E;98Attfwcno86PYhpUWbCfU3$N7k+zyFOxJmpi)- zc-wWryQ~A=WgYM?>wtGz2fWKV;9b@M@3Ibfmvz9qtOMR<9q=yefOlDMxUP>`&n5aX z>)A#>W<9|$e zw(&nEJlpsm6P|7Sj|tB<{>OyZ^)cbO#Q&J^Y~z1Sc((CBCOq5t9}}K!{ErFGHvY$i zXB+=x!n2M4G2wN6On5HwKPEie_#YFVZTyc3&o=(Ygl8N7W5TnI|1sg&#{YtTMEqSJ z5x-0HBjUG>enkAX(T|AVHu@3q+eSYke%t6r#BUq@i1@ocCSI3Z|Co4f=iz2@iGD=< zw$YD>-!}RY@!Li}B7WQGN5pR%{fPLxJ|cdX=tsnF8~p%)ZC?L~_-*t0N5pTN*FOMZ zo7X=ge%rkM5%JsB$u~6wI{6UAH#H18`B00qZ+7$0*@s;%u17@tT^|v@OZ<{}J(beMI~&@joJd+xQ<5zis@Fh~GB;N5pR%|0CkJjsFqRcYQ?kE*W1D z(c9+rjfmbhuWv;3w(&0_dfWIH5xs5vi-_Jf{;e3_5#e`zMEEYzj|ks3`VryVMn58a z+vrDxZyWuH@NJ_X5x#BoBf{_ci11x(!n7Gj6n> zY7uu+tb2Jc>ap%+8}(TCvK{Jz)`HfGm8(wBTF_e1TF_e1TF_e1TF_e1TF_e1TF_e1 zTF_e1TF_e1TF_e1TF_b0SetviHpGPm)3|Sn0r1Q75twDr8pUTgiN8;5uXEM~MeLs8u;l44Wby}SN z{NmmH!=s;yy}f^Te(T>z7!Chq$EmYzj(0Xo4xg7_*tNxb?(R0?+*%%m=OI* z7>+J(KJ{aohi={O5Bqi9QXhW4JJZ=-p>}cm#y=k6E}+LyRGrx~sGAKKE-p zJox*e`zdzm)%Zm3T-RX(a!d#GM+KK5?n zpBr&T%&_ZXzmivvc`+UypHSDs{Zk{me6V_qe?cc!bL%nN@tEx>>F3{nbjl$S@$kp-@W+o2zZiZ9sIEWP9{;p&c=1!a z<0u|qpa?9$Gk?|GPu~Q^()y;eY!!J_!`7$d zP}L)vtfy)>*-rHgk?J&5pN%+v4x)~VS(-fP{u(ONfJ>+!Dd*3J3X8qd?$`!4#>=VALKGE$l*b%voybEulGsc~K5 zA~_<%c`lpWko|d68mEYq#!HdXsF%{Hm-5)vlxtUqK6#D1t?w_dx9`vCb?ANnT;J<@ zop^sv*Y~=v?{$YY++Cl&uG+LO%{fbRY-!rhn)b7%pR?xJS<|0c)Bjm>&Utg5d2{T% zId-IcN-^N>PSl=42ZQr!-YyT&)Xf-zBR;qDH+28%0-x%%N_{~vMA1{O7WQ^OV`x(NE zSY!9S@YJ+v+NY-dc$;=!$Qj!=w}=;L#_hQ|e{R}0w~-e-#`ewd_X5YbecrTbhPf9b znl?@QWz)VHwth1|wr_^2-=vS*ubMtzHSMpO^RJrr&2aXcxA)C?)NEJXe%ZvY!!eCr$4g_^Vc6JpsAJc7wmH7Ww7;-x`;*%m zyPi6x9d-`^_q33`QOMpXWbqa9E(&=Ug}jSG-bEqrqL6n{$h#=yT@>;z3V9cWyo*BK zMIrB^katnYyC~#c6!I<#c^8G8yh2W1A?2cwa#2XRD5P8zQZ5Q97lo9ILdr!U<)V;s zQAoKcq+AqIE($3Zg_Mgz%0(gNqL6Y?NVzDaToh6+3Mm(bl#4>jMIq&)kaAH-xhSMu z6jCkz}+HbI2aidNZ_(mFsvL>6U<&!Uu4loo3tVF@bO5;j4Uu!NvIsEDT4 zSGJ(=w7|0vh)76S8bYVXu7@Y@*x0l@1;Xc1vQ^3->OeL7GRIk0Dsh4H&Jbd?VYUamFv9K(w6UAf2uqlJvlAz>5dgwICG8_FYG zzw}%b7K;Ez!vXCG0JdIX!hzbW(1ilw8o=m%3=E`fFb9O)$;v?cO$q?tD(Qj)*?-Wa zaj&jxylQF?PBv|mMoW4!J>D-DtbUM>P!-M5&1b@F(e)&LYIAUHj9E>%_fDnX+ixrJ zmYs&u{^MtRwb$I2hhX9SFaX*oRDSDDTZy+#pCkNmGyL9I~mEe|iJe86tqCZBD&PgXlCu64soCR3j z3WM^ddIIWYQ@qj1J091`XQeZR_>EeO8xBZ9$Kq={@&>o7=JA$S(v!+>4_lVo;TAr2 zXw5UETr)YiOtW#eq>?cj4ce7-TZut;3}CIKTl~~DTnh~$;|h1>4K%j${o;2>XNkO` zbRoh*=1Fk-gN$eLYJt*;w#Ho#Siy-d}o|2JA-Eitpe(o zsFr2(#$ExTJ69Rx?T5)3Qw1tPs}$s#qO5-2016Bheh|DUm(~ch+8r>y?#)__YHQlW z7`)^YkJrFJU(?I95J;Kf>hV?p#HZHV#&7i@COxz8IprrxFG)(NVVa^|RR(lB>ow5- z4Y^A+N^uqrdjZoarva|E;Ohi1c1pT+ZBUW$g&927oSx4&DVgPg1_#^HyNbWK4ev$g zvu$P8VKu#%qiOw7BvaZ|0c%nrOPD%6FrD3S!7f*x(<0YM!X{))44t9RdWTkfd@0T# zr*)YJ+>b=UHRxUF`h{?xD9NC0|E4N0!~V8-1UJ%FBi0-myUbN{*ldOl7k*zK;_p6v zvnvmYHoWI^AhQG0;816NR}U|wR?gh5YD(8fBGD1jDCq+sZmUmxTo|-RI$1;;< znbc2WrIm_Cj)Hnp#*>E4?Uzr#HiO^d@*y)i~!CVJu2o zXD%QXQ)O}5s!5JyWRsqLl6;8E?U7GAJ@mgd-r;#RA)G9$9@^I}$^J%!%I#DXvP8S_ z(j<;7i9kI?;^lI?L$}kmd>kRJ^k0mv4CBk3L}IkO-%vi$(_qUgNA^)zXlI5vTD%}V zF4JR(_9e!UuJF>Dk25xlQcVuV7T-6~bvMhqM=NZ2hmpGDCSojl|!qB-hydG#LriI(R!jtbHaxFh(t2_t`iFd6Pr+sm`~Ny z;_|37HXD5)ZAtgft0To%;-7@YJw53Q!+FKysSc6P7gWDOk&5K?qe3f#Pu*G#x69bM}6dU1Z76A-C$` zUrBfmspckWo7cFc&!w^kfE zl9m%OaQx>$TPIs)V13@(WVQ;f{|O5iB3rcZLw3=f*f%K17A<mj1SpMoHyO4wxJF5&>Y5Bi|=GGPqNt)7m}d0J13oK{KrhU(2|a zD1463IjZe1)jMFbMCRc{%0MSg!%(q!4Uw!LgB$hXrkY=Y&IjM+$EPpz;TLx73!h_{awA zF6d^WbgwCcjm7v&o~K|yTmf-~tX0bXe!>xIxZ}o9{XndrD0x1jXbC}zvwzwdGbc&wbp8E(6CVsq&D6xU0eXyaF?V<~`O(_1K znFAvijJ|1{Ln6SKOVvxcuYVP7{a#jZZq>v|K5z1E+#+ql%qI2w#tI2eiGYA+kXzp7 z-H_C}yC(@l+3ImWCMfT3JB{&+8iOa8Ry4&vUl>*eWa>=>-#)jh>9D@*mqrA}eA+8Z z&E%mozDsy!*9{4$dTV!0)A_BZ46FCfXa2{*hJlsttBdFej}}Cu8*$^Y9A0KoS z;xwOoF0B7YJ;HW!ZP)O^Oe0U8|2C~QC|_KIgr<)&LaTcVxxJEVn&T1l&H4xUehGEv zrd4?2%}{Z#v`y*^eqz!5gFb6xj?;p-K@tc*w#Az`g-Rz!_)FA?%E0k+ zM!>}w`+JlQny-CEX*AcNPK$TUwX>RAFSe@hz?Tc^Ly*8RB~gi8{cbmQZ||6WfyF=B z)ly%XJ%>Lpt_wipy)h+YjfRA>S?RV?uky&m(4(Vm}aP-sQ|# zhiGnER|VV-9dRi;h`I1z!<7^) literal 0 HcmV?d00001 diff --git a/test/analysis/contour_area/normal.slvs b/test/analysis/contour_area/normal.slvs new file mode 100644 index 0000000..15dc34a --- /dev/null +++ b/test/analysis/contour_area/normal.slvs @@ -0,0 +1,512 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00060013 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00060014 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00070010 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00070011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00070013 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00070014 +Param.val=10.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000007 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00060001 +Entity.point[1].v=00060002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00070000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00070001 +Entity.point[1].v=00070002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00070001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00070002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040001 +Constraint.ptB.v=00050002 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050001 +Constraint.ptB.v=00060002 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000003 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00060001 +Constraint.ptB.v=00070002 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000004 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00070001 +Constraint.ptB.v=00040002 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000005 +Constraint.type=81 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000006 +Constraint.type=80 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000007 +Constraint.type=81 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00060000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000008 +Constraint.type=80 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00070000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/analysis/contour_area/test.cpp b/test/analysis/contour_area/test.cpp new file mode 100644 index 0000000..eae410d --- /dev/null +++ b/test/analysis/contour_area/test.cpp @@ -0,0 +1,9 @@ +#include "solvespace.h" + +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + SS.showContourAreas = true; + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); +} diff --git a/test/commit.sh b/test/commit.sh new file mode 100755 index 0000000..d49708e --- /dev/null +++ b/test/commit.sh @@ -0,0 +1,9 @@ +#!/bin/sh -ex + +make -C build solvespace-testsuite +./build/test/solvespace-testsuite $* || true +for e in slvs png; do + for i in `find . -name *.out.$e`; do + mv $i `dirname $i`/`basename $i .out.$e`.$e; + done; +done diff --git a/test/constraint/angle/free_in_3d.png b/test/constraint/angle/free_in_3d.png new file mode 100644 index 0000000000000000000000000000000000000000..2fef8c185e0a3efea5f6b4e411d23675eeaf05ab GIT binary patch literal 4763 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 Usb)9`KRSWUCKkr54QSB60EA524gdfE literal 0 HcmV?d00001 diff --git a/test/constraint/angle/free_in_3d.slvs b/test/constraint/angle/free_in_3d.slvs new file mode 100644 index 0000000..dd9a053 --- /dev/null +++ b/test/constraint/angle/free_in_3d.slvs @@ -0,0 +1,355 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040013 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040015 +AddParam + +Param.h.v.=00050010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Param.h.v.=00050013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=5.00000000000000088818 +AddParam + +Param.h.v.=00050015 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000088818 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.ptA.v=00040002 +Constraint.ptB.v=00050001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=120 +Constraint.group.v=00000002 +Constraint.valA=45.00000000000000000000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=1 +Constraint.other2=0 +Constraint.reference=0 +Constraint.disp.offset.x=0.51894056308654845644 +Constraint.disp.offset.y=3.99584233576642278152 +AddConstraint + diff --git a/test/constraint/angle/free_in_3d_v20.slvs b/test/constraint/angle/free_in_3d_v20.slvs new file mode 100644 index 0000000..7f118f9 --- /dev/null +++ b/test/constraint/angle/free_in_3d_v20.slvs @@ -0,0 +1,355 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040013 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040015 +AddParam + +Param.h.v.=00050010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Param.h.v.=00050013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=5.00000000000000090000 +AddParam + +Param.h.v.=00050015 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000090000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.ptA.v=00040002 +Constraint.ptB.v=00050001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=120 +Constraint.group.v=00000002 +Constraint.valA=45.00000000000000000000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=1 +Constraint.other2=0 +Constraint.reference=0 +Constraint.disp.offset.x=0.51894056308654846000 +Constraint.disp.offset.y=3.99584233576642280000 +AddConstraint + diff --git a/test/constraint/angle/free_in_3d_v22.slvs b/test/constraint/angle/free_in_3d_v22.slvs new file mode 100644 index 0000000..488f552 --- /dev/null +++ b/test/constraint/angle/free_in_3d_v22.slvs @@ -0,0 +1,355 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040013 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040015 +AddParam + +Param.h.v.=00050010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Param.h.v.=00050013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=5.00000000000000088818 +AddParam + +Param.h.v.=00050015 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000088818 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.ptA.v=00040002 +Constraint.ptB.v=00050001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=120 +Constraint.group.v=00000002 +Constraint.valA=45.00000000000000000000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=1 +Constraint.other2=0 +Constraint.reference=0 +Constraint.disp.offset.x=0.51894056308654845644 +Constraint.disp.offset.y=3.99584233576642278152 +AddConstraint + diff --git a/test/constraint/angle/normal.png b/test/constraint/angle/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..40b2d0e190cf5f5c833be4f3730398c704e3c0cb GIT binary patch literal 4882 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 literal 0 HcmV?d00001 diff --git a/test/constraint/angle/reference_free_in_3d.slvs b/test/constraint/angle/reference_free_in_3d.slvs new file mode 100644 index 0000000..bfc1723 --- /dev/null +++ b/test/constraint/angle/reference_free_in_3d.slvs @@ -0,0 +1,355 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040013 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040015 +AddParam + +Param.h.v.=00050010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Param.h.v.=00050013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=5.00000000000000088818 +AddParam + +Param.h.v.=00050015 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000088818 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.ptA.v=00040002 +Constraint.ptB.v=00050001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=120 +Constraint.group.v=00000002 +Constraint.valA=45.00000000000000000000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=1 +Constraint.other2=0 +Constraint.reference=1 +Constraint.disp.offset.x=0.51894056308654845644 +Constraint.disp.offset.y=3.99584233576642278152 +AddConstraint + diff --git a/test/constraint/angle/reference_free_in_3d_v20.slvs b/test/constraint/angle/reference_free_in_3d_v20.slvs new file mode 100644 index 0000000..8a6aef6 --- /dev/null +++ b/test/constraint/angle/reference_free_in_3d_v20.slvs @@ -0,0 +1,355 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040013 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040015 +AddParam + +Param.h.v.=00050010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Param.h.v.=00050013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=5.00000000000000090000 +AddParam + +Param.h.v.=00050015 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000090000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.ptA.v=00040002 +Constraint.ptB.v=00050001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=120 +Constraint.group.v=00000002 +Constraint.valA=45.00000000000000000000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=1 +Constraint.other2=0 +Constraint.reference=1 +Constraint.disp.offset.x=0.51894056308654846000 +Constraint.disp.offset.y=3.99584233576642280000 +AddConstraint + diff --git a/test/constraint/angle/reference_free_in_3d_v22.slvs b/test/constraint/angle/reference_free_in_3d_v22.slvs new file mode 100644 index 0000000..dd91a68 --- /dev/null +++ b/test/constraint/angle/reference_free_in_3d_v22.slvs @@ -0,0 +1,355 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040013 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040015 +AddParam + +Param.h.v.=00050010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Param.h.v.=00050013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=5.00000000000000088818 +AddParam + +Param.h.v.=00050015 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000088818 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.ptA.v=00040002 +Constraint.ptB.v=00050001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=120 +Constraint.group.v=00000002 +Constraint.valA=45.00000000000000000000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=1 +Constraint.other2=0 +Constraint.reference=1 +Constraint.disp.offset.x=0.51894056308654845644 +Constraint.disp.offset.y=3.99584233576642278152 +AddConstraint + diff --git a/test/constraint/angle/reference_v20.slvs b/test/constraint/angle/reference_v20.slvs new file mode 100644 index 0000000..1ba8cb6 --- /dev/null +++ b/test/constraint/angle/reference_v20.slvs @@ -0,0 +1,366 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=5.00000000000000090000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000090000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040002 +Constraint.ptB.v=00050001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=120 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=45.00000000000000000000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=1 +Constraint.other2=0 +Constraint.reference=1 +Constraint.disp.offset.y=4.50000000000000000000 +AddConstraint + +Constraint.h.v=00000003 +Constraint.type=120 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=135.00000000000000000000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=1 +Constraint.disp.offset.x=-1.99273176225234480000 +Constraint.disp.offset.y=1.12091161626694390000 +AddConstraint + diff --git a/test/constraint/angle/reference_v22.slvs b/test/constraint/angle/reference_v22.slvs new file mode 100644 index 0000000..0408a83 --- /dev/null +++ b/test/constraint/angle/reference_v22.slvs @@ -0,0 +1,366 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=5.00000000000000088818 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000088818 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040002 +Constraint.ptB.v=00050001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=120 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=45.00000000000000000000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=1 +Constraint.other2=0 +Constraint.reference=1 +Constraint.disp.offset.y=4.50000000000000000000 +AddConstraint + +Constraint.h.v=00000003 +Constraint.type=120 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=135.00000000000000000000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=1 +Constraint.disp.offset.x=-1.99273176225234482928 +Constraint.disp.offset.y=1.12091161626694391096 +AddConstraint + diff --git a/test/constraint/angle/skew.png b/test/constraint/angle/skew.png new file mode 100644 index 0000000000000000000000000000000000000000..92d7577a44f841963122399714b2e8daae3d7690 GIT binary patch literal 4867 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 literal 0 HcmV?d00001 diff --git a/test/constraint/angle/skew.slvs b/test/constraint/angle/skew.slvs new file mode 100644 index 0000000..e793f93 --- /dev/null +++ b/test/constraint/angle/skew.slvs @@ -0,0 +1,340 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=00020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=00020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=00020000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=00020000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=120 +Constraint.group.v=00000002 +Constraint.valA=90.00000000000000000000 +Constraint.entityA.v=00050000 +Constraint.entityB.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +Constraint.disp.offset.x=2.50000000000000044409 +Constraint.disp.offset.y=-2.50000000000000000000 +Constraint.disp.offset.z=-2.50000000000000000000 +AddConstraint + diff --git a/test/constraint/angle/skew_v22.slvs b/test/constraint/angle/skew_v22.slvs new file mode 100644 index 0000000..e793f93 --- /dev/null +++ b/test/constraint/angle/skew_v22.slvs @@ -0,0 +1,340 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=00020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=00020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=00020000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=00020000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=120 +Constraint.group.v=00000002 +Constraint.valA=90.00000000000000000000 +Constraint.entityA.v=00050000 +Constraint.entityB.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +Constraint.disp.offset.x=2.50000000000000044409 +Constraint.disp.offset.y=-2.50000000000000000000 +Constraint.disp.offset.z=-2.50000000000000000000 +AddConstraint + diff --git a/test/constraint/angle/test.cpp b/test/constraint/angle/test.cpp new file mode 100644 index 0000000..668394d --- /dev/null +++ b/test/constraint/angle/test.cpp @@ -0,0 +1,70 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(free_in_3d_roundtrip) { + CHECK_LOAD("free_in_3d.slvs"); + CHECK_RENDER("free_in_3d.png"); + CHECK_SAVE("free_in_3d.slvs"); +} + +TEST_CASE(free_in_3d_migrate_from_v20) { + CHECK_LOAD("free_in_3d_v20.slvs"); + CHECK_SAVE("free_in_3d.slvs"); +} + +TEST_CASE(free_in_3d_migrate_from_v22) { + CHECK_LOAD("free_in_3d_v22.slvs"); + CHECK_SAVE("free_in_3d.slvs"); +} + +TEST_CASE(reference_roundtrip) { + CHECK_LOAD("reference.slvs"); + CHECK_RENDER("reference.png"); + CHECK_SAVE("reference.slvs"); +} + +TEST_CASE(reference_migrate_from_v20) { + CHECK_LOAD("reference_v20.slvs"); + CHECK_SAVE("reference.slvs"); +} + +TEST_CASE(reference_migrate_from_v22) { + CHECK_LOAD("reference_v22.slvs"); + CHECK_SAVE("reference.slvs"); +} + +TEST_CASE(reference_free_in_3d_roundtrip) { + CHECK_LOAD("reference_free_in_3d.slvs"); + CHECK_RENDER("reference_free_in_3d.png"); + CHECK_SAVE("reference_free_in_3d.slvs"); +} + +TEST_CASE(reference_free_in_3d_migrate_from_v20) { + CHECK_LOAD("reference_free_in_3d_v20.slvs"); + CHECK_SAVE("reference_free_in_3d.slvs"); +} + +TEST_CASE(reference_free_in_3d_migrate_from_v22) { + CHECK_LOAD("reference_free_in_3d_v22.slvs"); + CHECK_SAVE("reference_free_in_3d.slvs"); +} + +TEST_CASE(skew_render) { + CHECK_LOAD("skew.slvs"); + CHECK_RENDER("skew.png"); +} diff --git a/test/constraint/arc_line_tangent/normal.png b/test/constraint/arc_line_tangent/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..ab59db735cdd656b8eff17463b758c7b6c2add85 GIT binary patch literal 5100 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 literal 0 HcmV?d00001 diff --git a/test/constraint/arc_line_tangent/normal.slvs b/test/constraint/arc_line_tangent/normal.slvs new file mode 100644 index 0000000..7572139 --- /dev/null +++ b/test/constraint/arc_line_tangent/normal.slvs @@ -0,0 +1,450 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00040016 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040017 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=25.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060013 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00060014 +Param.val=-5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=500 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=14000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.point[2].v=00040003 +Entity.normal.v=00040020 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040003 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=25.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00060001 +Entity.point[1].v=00060002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040002 +Constraint.ptB.v=00050001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=123 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000003 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040003 +Constraint.ptB.v=00060001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000004 +Constraint.type=123 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00060000 +Constraint.other=1 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/arc_line_tangent/normal_v20.slvs b/test/constraint/arc_line_tangent/normal_v20.slvs new file mode 100644 index 0000000..402d9de --- /dev/null +++ b/test/constraint/arc_line_tangent/normal_v20.slvs @@ -0,0 +1,450 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00040016 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040017 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=25.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060013 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00060014 +Param.val=-5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=500 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=14000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.point[2].v=00040003 +Entity.normal.v=00040020 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040003 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=25.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00060001 +Entity.point[1].v=00060002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040002 +Constraint.ptB.v=00050001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=123 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000003 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040003 +Constraint.ptB.v=00060001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000004 +Constraint.type=123 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00060000 +Constraint.other=1 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/arc_line_tangent/normal_v22.slvs b/test/constraint/arc_line_tangent/normal_v22.slvs new file mode 100644 index 0000000..86a7d82 --- /dev/null +++ b/test/constraint/arc_line_tangent/normal_v22.slvs @@ -0,0 +1,450 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00040016 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040017 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=25.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060013 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00060014 +Param.val=-5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=500 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=14000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.point[2].v=00040003 +Entity.normal.v=00040020 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040003 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=25.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00060001 +Entity.point[1].v=00060002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040002 +Constraint.ptB.v=00050001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=123 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000003 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040003 +Constraint.ptB.v=00060001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000004 +Constraint.type=123 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00060000 +Constraint.other=1 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/arc_line_tangent/test.cpp b/test/constraint/arc_line_tangent/test.cpp new file mode 100644 index 0000000..5e9db1b --- /dev/null +++ b/test/constraint/arc_line_tangent/test.cpp @@ -0,0 +1,17 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} diff --git a/test/constraint/at_midpoint/line_plane_free_in_3d.png b/test/constraint/at_midpoint/line_plane_free_in_3d.png new file mode 100644 index 0000000000000000000000000000000000000000..9f41d0f858083c5306934fe5631e26329cccd5aa GIT binary patch literal 4333 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 literal 0 HcmV?d00001 diff --git a/test/constraint/at_midpoint/line_pt_free_in_3d.slvs b/test/constraint/at_midpoint/line_pt_free_in_3d.slvs new file mode 100644 index 0000000..f40d499 --- /dev/null +++ b/test/constraint/at_midpoint/line_pt_free_in_3d.slvs @@ -0,0 +1,315 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040015 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=70 +Constraint.group.v=00000002 +Constraint.ptA.v=00050000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/at_midpoint/line_pt_free_in_3d_v20.slvs b/test/constraint/at_midpoint/line_pt_free_in_3d_v20.slvs new file mode 100644 index 0000000..63806c5 --- /dev/null +++ b/test/constraint/at_midpoint/line_pt_free_in_3d_v20.slvs @@ -0,0 +1,313 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040015 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=70 +Constraint.group.v=00000002 +Constraint.ptA.v=00050000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/at_midpoint/line_pt_free_in_3d_v22.slvs b/test/constraint/at_midpoint/line_pt_free_in_3d_v22.slvs new file mode 100644 index 0000000..2933389 --- /dev/null +++ b/test/constraint/at_midpoint/line_pt_free_in_3d_v22.slvs @@ -0,0 +1,315 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040015 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=70 +Constraint.group.v=00000002 +Constraint.ptA.v=00050000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/at_midpoint/line_pt_normal.png b/test/constraint/at_midpoint/line_pt_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..0e43d59a86ebede65c3dbbc7f22c5fb364216db1 GIT binary patch literal 4298 zcmeHLYgAI{8va03l+@C~E(%RecEd}Vm!iZeFEv^nCnLmE%OaS?%#10BG9qg*8A{s<&r10x=$p?>r1=LdL4Ey^YmHttLz&Q!0oY;r9y{-yZ*l9T=uDCOJ>7e%Gc0BlL( zNdbU%V(`E{8$tuD1#(6x8V6?jGEm^K5ecvzIQn3sjSzreKi4MNox95=0^8c6Ye`RE zcFUILss#~=%0QV077tnm_@u`cE-kvz($U9iV!2us5j^Wn#8}kF!s*HD_#MqjpAzJv z8}^2Qe#h?tz%IJ}zPIfOJHv|x!qhi#L~^!0Fas0rU`AnST~HDBiGZ+^NIq&)wsi74 zVKDayq!<9L_I)q1+HASP8idap+dvCqOjGNDLyJcvhsusT%l^a+af!9xRq1hE5bSi) z`RD;XkmrH|4(1l`OSL=YX?dI>IE5YCK((ZKX#vC?i`=2`9C`aEQl_J(*k1)Y*4~d? zrNb|05ezHV0+4od4CHsXcP}HhP>O(umG>qqdeAH0eb4#Q%E>=vXbReCARMaLdxj+{}O5d77!t|-vFjx+O?J8D;G z7hk3)hcxu`u_GQEL3@3L&8A{~R7@6haE6-=HGdPRT*bI<{i`uM+K3e6z7_S{R7Xfg z%!7yH$J&vd8oN6@yMmvaLR2J(5jUB->Atn&yZgsCn>1#?x9b*;souyBpQ1CFY8&9( zFQpIoRT;76W7_jU(?W!*dvnxoz+Q2JfjXQ}Bn7OgXxz*;?}2FnD{-d(8j%aE=Ki3O zZF40C51IOwG&f@KtnlPuD{)dHgEBa@{+Paewve6l8kR*9L!^r*(^&*Gt{IoT{d&1C zq7`kwc8d#Wd_ECndq~)sQPb%VM0kD0U^8*8_ zx8*ACt#+m$vwsIG@l|VWboYE>9gk>6!Tq>E@z02mBrmpwMtXQk4q`x>MSU)6XVJke zp3$|#fn|wN=x{f)nlCt;N`rO{7uhTYc__(LN6_rkQhS#}78yUnq zQaEGW1IO|EsrAJ_=TQGwA9=gc`2xd834Vd${|JWD_Ci=;0ze))gdwsi2JE2Ij)+)B zeXXOI$O|Fzf4=RCPB95Dp6=S?LLqedSKt7HF(%I+Z1zV*9YAMBMfaZB@rdS)|J_OI zP7i!%VM47FaNs_M==fv$U_!wl{_HI@=q)DADG-?P;X8!o03smf1c2V~SLld%+|1I% zT-nfR2iP1$vfmPmaoX)fgUs9@EU=>Ga@q|&^nf9}gCTMb-zw+|j}VxF)=n28eTH)} z-Ga`l<0eZv*2pNJG3CQ4NBI}EnlN)uJm@JHf_CpIyS2MZc5KcSz$J!Zs7%UirbB)J zxK~gCO%H!^Wo{h_bkvF!CV#d19rRsK$undQI8_36>r^?u#Yjy^1rC?KnM*UW+u@RcyeP9gS&UOSDU|`M(I?6p41EYrTG7Nk@8@UhV zKfS6oYi~XJRUzPp$PRTT=%+l_n)b-0r@1mAc#P z7s)nt{Ul&#wNO}E5lvRXsudM#7fC+#o<(vnXI;-UT1I09kR2geWGqkaN4YZva;PFb z-?r|8@`5?$p5;&&X4onAr4f4*tiKhzMcJp6N}pbxP+U~$^lIIMrpL09NIPOfi6pl7 zvS6Sxs!A0v4wnV=HAR-XMMz-098~UH@!KLvMslJ`_UFskB7S*&Yi#e*3_@+DB>q74 zCQ9$!?knxeELvaF5Y?@uMi4T$ruqkYQwysy4jH60Ri3UVy^mKYZ~xSJyZ+^Cz31mD zpk+6h^GmB^JExt&0oP;IA%`ND2DmB$?KXSCRoWo8l=w@t|C07o269xQXC-PLjB1{Y zkgso6=Y{({mz3oqjFBQr>KUcBpZ zNllVc$x_9}Z+y~k$n|OwY5Y|_EiVql#O$>u{xF2MGWDKY6C=j*{Cy>Wz3Nhx=PS}` zH%GX^oE*>3U8dTx<#!ECjMd(Z3~Q6u43EX}%9OQT@_D#)=Feg!XkIen*!3SP867S7 d9CQ7kp~QMM`RjAYe+tNFjpsU#!j+7J{{ajfuqFTi literal 0 HcmV?d00001 diff --git a/test/constraint/at_midpoint/line_pt_normal.slvs b/test/constraint/at_midpoint/line_pt_normal.slvs new file mode 100644 index 0000000..483bc76 --- /dev/null +++ b/test/constraint/at_midpoint/line_pt_normal.slvs @@ -0,0 +1,313 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=70 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/at_midpoint/line_pt_normal_v20.slvs b/test/constraint/at_midpoint/line_pt_normal_v20.slvs new file mode 100644 index 0000000..76d63eb --- /dev/null +++ b/test/constraint/at_midpoint/line_pt_normal_v20.slvs @@ -0,0 +1,311 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=70 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/at_midpoint/line_pt_normal_v22.slvs b/test/constraint/at_midpoint/line_pt_normal_v22.slvs new file mode 100644 index 0000000..07761c1 --- /dev/null +++ b/test/constraint/at_midpoint/line_pt_normal_v22.slvs @@ -0,0 +1,313 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=70 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/at_midpoint/test.cpp b/test/constraint/at_midpoint/test.cpp new file mode 100644 index 0000000..3525df9 --- /dev/null +++ b/test/constraint/at_midpoint/test.cpp @@ -0,0 +1,65 @@ +#include "harness.h" + +TEST_CASE(line_pt_normal_roundtrip) { + CHECK_LOAD("line_pt_normal.slvs"); + CHECK_RENDER("line_pt_normal.png"); + CHECK_SAVE("line_pt_normal.slvs"); +} + +TEST_CASE(line_pt_normal_migrate_from_v20) { + CHECK_LOAD("line_pt_normal_v20.slvs"); + CHECK_SAVE("line_pt_normal.slvs"); +} + +TEST_CASE(line_pt_normal_migrate_from_v22) { + CHECK_LOAD("line_pt_normal_v22.slvs"); + CHECK_SAVE("line_pt_normal.slvs"); +} + +TEST_CASE(line_pt_free_in_3d_roundtrip) { + CHECK_LOAD("line_pt_free_in_3d.slvs"); + CHECK_RENDER("line_pt_free_in_3d.png"); + CHECK_SAVE("line_pt_free_in_3d.slvs"); +} + +TEST_CASE(line_pt_free_in_3d_migrate_from_v20) { + CHECK_LOAD("line_pt_free_in_3d_v20.slvs"); + CHECK_SAVE("line_pt_free_in_3d.slvs"); +} + +TEST_CASE(line_pt_free_in_3d_migrate_from_v22) { + CHECK_LOAD("line_pt_free_in_3d_v22.slvs"); + CHECK_SAVE("line_pt_free_in_3d.slvs"); +} + +TEST_CASE(line_plane_normal_roundtrip) { + CHECK_LOAD("line_plane_normal.slvs"); + CHECK_RENDER("line_plane_normal.png"); + CHECK_SAVE("line_plane_normal.slvs"); +} + +TEST_CASE(line_plane_normal_migrate_from_v20) { + CHECK_LOAD("line_plane_normal_v20.slvs"); + CHECK_SAVE("line_plane_normal.slvs"); +} + +TEST_CASE(line_plane_normal_migrate_from_v22) { + CHECK_LOAD("line_plane_normal_v22.slvs"); + CHECK_SAVE("line_plane_normal.slvs"); +} + +TEST_CASE(line_plane_free_in_3d_roundtrip) { + CHECK_LOAD("line_plane_free_in_3d.slvs"); + CHECK_RENDER("line_plane_free_in_3d.png"); + CHECK_SAVE("line_plane_free_in_3d.slvs"); +} + +TEST_CASE(line_plane_free_in_3d_migrate_from_v20) { + CHECK_LOAD("line_plane_free_in_3d_v20.slvs"); + CHECK_SAVE("line_plane_free_in_3d.slvs"); +} + +TEST_CASE(line_plane_free_in_3d_migrate_from_v22) { + CHECK_LOAD("line_plane_free_in_3d_v22.slvs"); + CHECK_SAVE("line_plane_free_in_3d.slvs"); +} diff --git a/test/constraint/comment/normal.png b/test/constraint/comment/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..5a7036c29f62f9621384c02232d7093602630908 GIT binary patch literal 4314 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 Oz{YiUR@^nLi2nmA5tzpS literal 0 HcmV?d00001 diff --git a/test/constraint/cubic_line_tangent/free_in_3d.slvs b/test/constraint/cubic_line_tangent/free_in_3d.slvs new file mode 100644 index 0000000..fe3e29a --- /dev/null +++ b/test/constraint/cubic_line_tangent/free_in_3d.slvs @@ -0,0 +1,397 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040013 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040015 +AddParam + +Param.h.v.=00040016 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040017 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040018 +AddParam + +Param.h.v.=00040019 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=0004001a +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=0004001b +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Param.h.v.=00050013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00050015 +AddParam + +Param.h.v.=40000002 +Param.val=-1.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=300 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=12000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.point[2].v=00040003 +Entity.point[3].v=00040004 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040003 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040004 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.ptA.v=00050001 +Constraint.ptB.v=00040001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=124 +Constraint.group.v=00000002 +Constraint.valP.v=40000002 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/cubic_line_tangent/free_in_3d_v20.slvs b/test/constraint/cubic_line_tangent/free_in_3d_v20.slvs new file mode 100644 index 0000000..2717b89 --- /dev/null +++ b/test/constraint/cubic_line_tangent/free_in_3d_v20.slvs @@ -0,0 +1,392 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040013 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040015 +AddParam + +Param.h.v.=00040016 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040017 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040018 +AddParam + +Param.h.v.=00040019 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=0004001a +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=0004001b +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Param.h.v.=00050013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00050015 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=300 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=12000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.point[2].v=00040003 +Entity.point[3].v=00040004 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040003 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040004 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.ptA.v=00050001 +Constraint.ptB.v=00040001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=124 +Constraint.group.v=00000002 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/cubic_line_tangent/free_in_3d_v22.slvs b/test/constraint/cubic_line_tangent/free_in_3d_v22.slvs new file mode 100644 index 0000000..d5ca7c6 --- /dev/null +++ b/test/constraint/cubic_line_tangent/free_in_3d_v22.slvs @@ -0,0 +1,392 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040013 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040015 +AddParam + +Param.h.v.=00040016 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040017 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040018 +AddParam + +Param.h.v.=00040019 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=0004001a +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=0004001b +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Param.h.v.=00050013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00050015 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=300 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=12000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.point[2].v=00040003 +Entity.point[3].v=00040004 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040003 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040004 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.ptA.v=00050001 +Constraint.ptB.v=00040001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=124 +Constraint.group.v=00000002 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/cubic_line_tangent/normal.png b/test/constraint/cubic_line_tangent/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..394f8ff73f425202b8707b3862be09473e65dcd0 GIT binary patch literal 5220 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 literal 0 HcmV?d00001 diff --git a/test/constraint/cubic_line_tangent/normal.slvs b/test/constraint/cubic_line_tangent/normal.slvs new file mode 100644 index 0000000..f8a534c --- /dev/null +++ b/test/constraint/cubic_line_tangent/normal.slvs @@ -0,0 +1,446 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +AddParam + +Param.h.v.=00040013 +AddParam + +Param.h.v.=00040014 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040016 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040017 +AddParam + +Param.h.v.=00040019 +AddParam + +Param.h.v.=0004001a +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060011 +AddParam + +Param.h.v.=00060013 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00060014 +Param.val=10.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=300 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=12000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.point[2].v=00040003 +Entity.point[3].v=00040004 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.y=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040003 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040004 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00060001 +Entity.point[1].v=00060002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040004 +Constraint.ptB.v=00050001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=124 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=1 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000003 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040001 +Constraint.ptB.v=00060001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000004 +Constraint.type=124 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00060000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/cubic_line_tangent/normal_v20.slvs b/test/constraint/cubic_line_tangent/normal_v20.slvs new file mode 100644 index 0000000..c141192 --- /dev/null +++ b/test/constraint/cubic_line_tangent/normal_v20.slvs @@ -0,0 +1,446 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +AddParam + +Param.h.v.=00040013 +AddParam + +Param.h.v.=00040014 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040016 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040017 +AddParam + +Param.h.v.=00040019 +AddParam + +Param.h.v.=0004001a +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060011 +AddParam + +Param.h.v.=00060013 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00060014 +Param.val=10.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=300 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=12000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.point[2].v=00040003 +Entity.point[3].v=00040004 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.y=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040003 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040004 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00060001 +Entity.point[1].v=00060002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040004 +Constraint.ptB.v=00050001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=124 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=1 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000003 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040001 +Constraint.ptB.v=00060001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000004 +Constraint.type=124 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00060000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/cubic_line_tangent/normal_v22.slvs b/test/constraint/cubic_line_tangent/normal_v22.slvs new file mode 100644 index 0000000..049bd73 --- /dev/null +++ b/test/constraint/cubic_line_tangent/normal_v22.slvs @@ -0,0 +1,446 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +AddParam + +Param.h.v.=00040013 +AddParam + +Param.h.v.=00040014 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040016 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040017 +AddParam + +Param.h.v.=00040019 +AddParam + +Param.h.v.=0004001a +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060011 +AddParam + +Param.h.v.=00060013 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00060014 +Param.val=10.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=300 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=12000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.point[2].v=00040003 +Entity.point[3].v=00040004 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.y=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040003 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040004 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00060001 +Entity.point[1].v=00060002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040004 +Constraint.ptB.v=00050001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=124 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=1 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000003 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040001 +Constraint.ptB.v=00060001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000004 +Constraint.type=124 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00060000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/cubic_line_tangent/test.cpp b/test/constraint/cubic_line_tangent/test.cpp new file mode 100644 index 0000000..4dcc334 --- /dev/null +++ b/test/constraint/cubic_line_tangent/test.cpp @@ -0,0 +1,33 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(free_in_3d_roundtrip) { + CHECK_LOAD("free_in_3d.slvs"); + CHECK_RENDER("free_in_3d.png"); + CHECK_SAVE("free_in_3d.slvs"); +} + +TEST_CASE(free_in_3d_migrate_from_v20) { + CHECK_LOAD("free_in_3d_v20.slvs"); + CHECK_SAVE("free_in_3d.slvs"); +} + +TEST_CASE(free_in_3d_migrate_from_v22) { + CHECK_LOAD("free_in_3d_v22.slvs"); + CHECK_SAVE("free_in_3d.slvs"); +} diff --git a/test/constraint/curve_curve_tangent/arc_arc.png b/test/constraint/curve_curve_tangent/arc_arc.png new file mode 100644 index 0000000000000000000000000000000000000000..c83e0a8890f47f4c2cc4ae968c82f58c611fb0bd GIT binary patch literal 5027 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 MYrB&F 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 literal 0 HcmV?d00001 diff --git a/test/constraint/diameter/normal.slvs b/test/constraint/diameter/normal.slvs new file mode 100644 index 0000000..583b3ac --- /dev/null +++ b/test/constraint/diameter/normal.slvs @@ -0,0 +1,296 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=90 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=10.00000000000000000000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +Constraint.disp.offset.x=5.00000000000000000000 +Constraint.disp.offset.y=5.00000000000000000000 +AddConstraint + diff --git a/test/constraint/diameter/normal_v20.slvs b/test/constraint/diameter/normal_v20.slvs new file mode 100644 index 0000000..7427519 --- /dev/null +++ b/test/constraint/diameter/normal_v20.slvs @@ -0,0 +1,294 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=90 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=10.00000000000000000000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +Constraint.disp.offset.x=5.00000000000000000000 +Constraint.disp.offset.y=5.00000000000000000000 +AddConstraint + diff --git a/test/constraint/diameter/normal_v22.slvs b/test/constraint/diameter/normal_v22.slvs new file mode 100644 index 0000000..81a24e2 --- /dev/null +++ b/test/constraint/diameter/normal_v22.slvs @@ -0,0 +1,296 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=90 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=10.00000000000000000000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +Constraint.disp.offset.x=5.00000000000000000000 +Constraint.disp.offset.y=5.00000000000000000000 +AddConstraint + diff --git a/test/constraint/diameter/reference.png b/test/constraint/diameter/reference.png new file mode 100644 index 0000000000000000000000000000000000000000..09a3f28ea3d4013f8a291d7f5269b3b5a9240f2d GIT binary patch literal 5101 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 literal 0 HcmV?d00001 diff --git a/test/constraint/diameter/reference.slvs b/test/constraint/diameter/reference.slvs new file mode 100644 index 0000000..b403a72 --- /dev/null +++ b/test/constraint/diameter/reference.slvs @@ -0,0 +1,296 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=90 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=10.00000000000000000000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=1 +Constraint.disp.offset.x=5.00000000000000000000 +Constraint.disp.offset.y=5.00000000000000000000 +AddConstraint + diff --git a/test/constraint/diameter/reference_v20.slvs b/test/constraint/diameter/reference_v20.slvs new file mode 100644 index 0000000..eb80eb9 --- /dev/null +++ b/test/constraint/diameter/reference_v20.slvs @@ -0,0 +1,296 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=90 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=10.00000000000000000000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=1 +Constraint.disp.offset.x=5.00000000000000000000 +Constraint.disp.offset.y=5.00000000000000000000 +AddConstraint + diff --git a/test/constraint/diameter/reference_v22.slvs b/test/constraint/diameter/reference_v22.slvs new file mode 100644 index 0000000..4f8455b --- /dev/null +++ b/test/constraint/diameter/reference_v22.slvs @@ -0,0 +1,296 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=90 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=10.00000000000000000000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=1 +Constraint.disp.offset.x=5.00000000000000000000 +Constraint.disp.offset.y=5.00000000000000000000 +AddConstraint + diff --git a/test/constraint/diameter/test.cpp b/test/constraint/diameter/test.cpp new file mode 100644 index 0000000..7e2a06e --- /dev/null +++ b/test/constraint/diameter/test.cpp @@ -0,0 +1,33 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(reference_roundtrip) { + CHECK_LOAD("reference.slvs"); + CHECK_RENDER("reference.png"); + CHECK_SAVE("reference.slvs"); +} + +TEST_CASE(reference_migrate_from_v20) { + CHECK_LOAD("reference_v20.slvs"); + CHECK_SAVE("reference.slvs"); +} + +TEST_CASE(reference_migrate_from_v22) { + CHECK_LOAD("reference_v22.slvs"); + CHECK_SAVE("reference.slvs"); +} diff --git a/test/constraint/eq_len_pt_line_d/normal.png b/test/constraint/eq_len_pt_line_d/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..9daa892bd79c2e83b96f006078e05c37d84551fe GIT binary patch literal 4407 zcmeHLc~DdL8vO+l3bM%V0t#xyzDFQYt1Pm(fPjElH};T<0tGQ3J7G!C%3vu>A5Vi; zVSGGLB9W*s1c8vNBB(4{(Xa=25h(@;5+H>b@)86~#8BIr&h(ErlgypDx%cM#&UeoF zPLi>2uao+cl}i8s>MqU>`vFk!lD?IdBxkCFaytRgoOW^8aWMYUP*-mxhoyI>_f*5y zEC11=_nOD<(+#S0Hi$K7Gf#Wl8nV+=+i{OkbOb6tSc&89s$~91Z)}Q?cAng>3JAH( z7!&~5tqQ8Z*bNHe8n$!nBaXh1~q^ zsL4qqt2|K$bM7Tx*5+FlGh}t6F~}e;icM`hE;!-duTD~i{Z@6E1p`4u2)fv0AX7KB zFh$g42*6G^xeao>T88yNr^6f`)M{wc>(7A6v9x!%5E8cdZ~R0=Aj>ie9sz|cEQ35T z>>D!ka z%EQ0`=VD;!i$(l1G73Ci{C8%fKAUPkM0@L3 zL4rZpzryGs?;#JonLTOv7(g%Ak!K>1C5whf01K@-J1IfE18fcfEM@^zGV1*_fXEEI zR>WapGp7DYA$|o=tCgd%x4LVVev|rGOcOMp3BdYyHLhBIk3Njpp6(m?FnlHWMR~?% zrWC^bJ2ccm#OHH;HC!J!dqHL>^1ZY6W_Xt=*xek>)|_j4=3=u$v^bPPQ)p^?*yNpP zlp^77?#EaF%qBM22R^8R>wl6R+{T$X9ow7gH`$5{C|i=3VB7S78)=SNsu48nUErSI z+LsYR`w8+_xYoAoBDiLf%fP$Bc{~&h&Z{lFQ19YzrwV$yZ(mD9p!RH#|gSR z$nsYKU8>mK)fe)=YdN(p7aLjL_vFEkySMq_bx>E|JPa_B!u?FwqT0?fS4FxuRP?Qu zPNnu*bb@UIIGJvYeD1B6!=Ti%347u|q{*dTr3>*^5SJk{N$y(;cZI2BBgmn@IpHAXo zK*UM~RpXaVqO-^=hz-x8A4rA`7hMy7t3Of>*N0$kw|j1?AbUEe%Udx*eun55p1q^K z&1xbdlY`@k#@_U;-*p9%y$cIVf;A)Kk*lwrY9;7#p5@l^^hXvEr5}2#eEEV`v~aU@y^ly2U`^&0@uJa* zt6CBhzU@l@{V>ZYv_=lD2O|kFpSZd~Ls*B8hUxEJ+w%~XVX5>y*3hIIVtklM=9}vW zxi7b+jAnRPW^82#U$7ak0qGElvpCZtCtzHtY;|-wbDU{Y(BCVBM05TiR*{oQu2x`aG^n)v}fb_hySSwFbvqMif9(V4mygr6FU^aDunH z_1%h19nue*6IUEuXQu?>;WowBj zkC62W>9Ko%O1t=AUVvJpq>98&A@=gv-ceMltwu!ezXRw1WqSha{)@E7sQ6BZtSy0a z&FXDbvV%U*-0<$Oo|>0|1jY_n|Imudb^uL^La^#pp84t&hUFCb3L?36+2?JhodoB6 zcJ)WKiL4c%ZzWey*cF!q?aa#90i3@(1^an5hPWehg;z^etU`*}EjOFdB)%enywZw_ z+1+f`e2w(gdSxtfx=vZTB{+u6!O3G^FeYx&kNnK4Pa^G@I7d>l*N!Qr@E9ULyE1`Y zKO?)VqKSmLA*8sL8MX181W{c61iSiy6^Lv1xJ}`X+cU?zJ4X`)gZyiCe0IMd9!XDP z(>Ptio7pxV^s1B$QOX$aaWE^M%RRa+mS5FzybMn-;@6gwl!K$f_X|#LI+{32vk`;N zVia|CFGH8kt{T(IQw8zHfy~diCvQcte9irKvwXRgNou%(EGj)q+}8j0ny8vCz99l5 znoU|>ILBw+1p^qA;%lD}#&~XJetJPx>Pt>VUpnH%3(;Fe^gkoJPmQ+=k5nfKH+CNYJn@t|f;t6Fafgg7_=(hT;#<&csYu%U{uq&JWk_Z?2BCmY}BOY m_sUjg9m+SWKkqLCF$mt^KW6VyvsLnk2yk)S>u_TyHtm1O@CZ== literal 0 HcmV?d00001 diff --git a/test/constraint/eq_len_pt_line_d/normal.slvs b/test/constraint/eq_len_pt_line_d/normal.slvs new file mode 100644 index 0000000..779430c --- /dev/null +++ b/test/constraint/eq_len_pt_line_d/normal.slvs @@ -0,0 +1,364 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060013 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00060014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00060001 +Entity.point[1].v=00060002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=52 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.entityA.v=00060000 +Constraint.entityB.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/eq_len_pt_line_d/normal_v20.slvs b/test/constraint/eq_len_pt_line_d/normal_v20.slvs new file mode 100644 index 0000000..6ce37fd --- /dev/null +++ b/test/constraint/eq_len_pt_line_d/normal_v20.slvs @@ -0,0 +1,362 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060013 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00060014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00060001 +Entity.point[1].v=00060002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=52 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.entityA.v=00060000 +Constraint.entityB.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/eq_len_pt_line_d/normal_v22.slvs b/test/constraint/eq_len_pt_line_d/normal_v22.slvs new file mode 100644 index 0000000..78b22b0 --- /dev/null +++ b/test/constraint/eq_len_pt_line_d/normal_v22.slvs @@ -0,0 +1,364 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060013 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00060014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00060001 +Entity.point[1].v=00060002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=52 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.entityA.v=00060000 +Constraint.entityB.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/eq_len_pt_line_d/test.cpp b/test/constraint/eq_len_pt_line_d/test.cpp new file mode 100644 index 0000000..5e9db1b --- /dev/null +++ b/test/constraint/eq_len_pt_line_d/test.cpp @@ -0,0 +1,17 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} diff --git a/test/constraint/eq_pt_ln_distances/normal.png b/test/constraint/eq_pt_ln_distances/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..4d040037bd765eb2d1166acec12fd7730eb4b5ca GIT binary patch literal 4363 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 Sy9<5f0Nb5*IOc639QzYcFW(CQ literal 0 HcmV?d00001 diff --git a/test/constraint/eq_pt_ln_distances/normal.slvs b/test/constraint/eq_pt_ln_distances/normal.slvs new file mode 100644 index 0000000..6a196e3 --- /dev/null +++ b/test/constraint/eq_pt_ln_distances/normal.slvs @@ -0,0 +1,389 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060013 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00060014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00070010 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00070011 +Param.val=10.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000007 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00060001 +Entity.point[1].v=00060002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00070000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=53 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.ptB.v=00070000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00060000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/eq_pt_ln_distances/normal_v20.slvs b/test/constraint/eq_pt_ln_distances/normal_v20.slvs new file mode 100644 index 0000000..2d878b9 --- /dev/null +++ b/test/constraint/eq_pt_ln_distances/normal_v20.slvs @@ -0,0 +1,387 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060013 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00060014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00070010 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00070011 +Param.val=10.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000007 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00060001 +Entity.point[1].v=00060002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00070000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=53 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.ptB.v=00070000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00060000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/eq_pt_ln_distances/normal_v22.slvs b/test/constraint/eq_pt_ln_distances/normal_v22.slvs new file mode 100644 index 0000000..1e4968b --- /dev/null +++ b/test/constraint/eq_pt_ln_distances/normal_v22.slvs @@ -0,0 +1,389 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060013 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00060014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00070010 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00070011 +Param.val=10.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000007 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00060001 +Entity.point[1].v=00060002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00070000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=53 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.ptB.v=00070000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00060000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/eq_pt_ln_distances/test.cpp b/test/constraint/eq_pt_ln_distances/test.cpp new file mode 100644 index 0000000..5e9db1b --- /dev/null +++ b/test/constraint/eq_pt_ln_distances/test.cpp @@ -0,0 +1,17 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} diff --git a/test/constraint/equal_angle/normal.png b/test/constraint/equal_angle/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..aecf31558739c25da5a58ab8caa8bad7e4c0a8b5 GIT binary patch literal 5271 zcmeHLc{tQ-`+sKajeSp9qV%33CWM5_mY3w5I*e^h26Zf1vc+IXIYdrJF-p|&(!wyM zAGVA?B9eIp+Q2^qrn}P37Ipj*yHUDG&7UH*`5f3?m_5K{6H3 z0TkZ)w4wkYB|=ERD+Zf8jzj?kOB@2k@TmZzdK4EBR;B>(h*J{mC;d%>MvlJBNX@1O zF3vO+r$;VnunWW6i$8xNFX8L)b{;dVo*wkrW7hfD*t)Ew-n!N~)vc~BMpis^EwY{oebi0E^Ku&+`Iv1sq6#;NRD6fWyWcy4);~RsMfOpEZ z1qnHWfktcM8g!B@n=6F?>SAJR-220*7zWZw$qwZL!6KhCledeKuz(<%r>(Ap!L}x9;?09^H|2Kd!Zr#XrQaUhQsg5KA!vk4?w2xgd!l*h_C^? zVY!zCosQ2XP9-P&&5 zh)MZon1!5eYEZ8|B7nE5hRKs~G(^{)w~3DR}<(ndSr6&v+p}5yj~gN@`=rl$!>jM@uV%)+XqWTqd6iw&Fw{zg{1d8z zEGajPtO((Q@NmyA%NJHhiy8(?CG9Sv+iaY05{U~pgI9X>^PgTn$ePkgB2PYCn9PKG z(=B3}MYdOjD!qOZ(a?WHG=uH_s9*YQ<3nTK>%z9vrX`maMFx_|K1-eGTCBJe8cu-V z&$*#WPNIbKcz$dHCn{cd;vO8N@(d{YAZ&wlCKR48gG9x{u{q#~a1h2=LvaN{Z#J~W zL(x*F->}X;L>6tObe~I~YrBf)Ur?ZQo21VzpjlJDqq!G_G4GI8ge|Ovp2Lppnz}4F zcM?&|#1p(KZ6q=0NcU@4-P#+5NYtLb>N>_|eDn4wu1$V#SWRBDD;>^o=}6b!x_`^| z3WG**%tiFzH9pB0-HB;)M?}F^ioz4vk;0{a;XE&*rO!dp!rLn%Xj(KG^rOvqb-^eu zgcSiVVhb@iB?-U4#sn+nPno_rG>g}*#qC3)44}C2nw0*>*Q7_?gX^Rd%yCu8pY~9@ zjnd6$Md;PZ#3X9c;B>SNBX_C~$8LbvlNqWFf(JKoqU5?^(23;OUPP15LeYA<6Nv+w zRGI$gH}L#>tOIIBj@Dyuo~WcP_Z+t(jO!=KV~gT<&zApaw6RXw5}rO5FgJS~hI5zn z@z7Q{aPEqf?)WCdh*O!79~^1?%5SC5o`H{)kIwa}h^4UsMugXBYUJYci_1DijCP4z zmBoyu*-;_cCHMt!m2)+0>aXHa_%B`s)Rq@V&CO z1IwN5=%8^l?}saVl8!n{lWl^_EwlB#0rjk@WPhW>K65Q+ToI>5Fure_Fljwz^b;Y1 zg;)E-G#Vy}0x<3(rQJj|f8UEJrC^tPNUD4)Cx7E2szlX{C5qcBktJ|VoRVem{o8gB z2DX#fx#VYWh6%JRvMkQWlG2u)hRkL;d+<|^A}@4oMCY42YCsu$DnTZRdFkG zkI<-XO=#l7*(hTy&3*>fhjHjDcD=vrQ91VAS*c+^1|^(Ir&0ELt+}-s~JXEG3>sbBK>q^0?nS}Xyb^g zv99I;O2Qca-YNHM8kIviL{44@=C2(QJ6`VGHz+RWW5VD+;2&zBu}=zcN^{kRFCc?&hx622B{iY-3**teAifKc0tvTOZ}FSr+4QjdlI96+v8%z?AeA>Z-)5{ zsf>zaYRK+A+9ewQn&h+b^y!+5(CrwmnP6uMRd*VZX0CEACq? zp)MVCf53hO=5(TJns=#5WGv-V7brujQy@fk7KbixFlWqnRR$+9Y@Uwa6SW0w*XSC} z&M=GEmb!xQ5hb>zj~H=+PbJ6KJ3eq;H`QS8OVOjC+ysYCJmF%dfS{Z!4Wp!FdoclU z@K4s;|KteFO-r{>z`=?K1(&uxxS{ls?Cft&o#|>u&RF0OUXUhg<}eNs&K@0^U}Wme z^e08etF*~SaV7Nn^e#?K%x^Qg$;F13z^;t!Z?+HXQ2R-5_F35iQ>${(q5T0&GZ#mT zBTGizyuE2Yk%s#3H^+yU40z6sFh{UOKJ2iU5mlZ>-Z+6N7hZAQN~VWX1vj%^NX@x1 zHa*AIxpPrP;qGni4|g{crRMJN?Q9c+P2)HE410TU)x_tg@TuJJ8AwI&lu@A<8&*7y zNwY_E<{%UfIU>kEE{Sh}TpdL@tv>P~iuRaaRsZnJV1l=T-n`K8shN=CgPQ1vBw4gDBt zw%4;!Ljs)gXNteB3d%+0xqa8J1)!8w&a5t&?9=_dNx`PCTV1Q4NX*id5t}tJ{{}(u zB|=)c822xa$^e|js>{-`_1f=l|C!#equS`6P8LBs*N}d_2aJoY7hpvg_nA?zD+`B_ zo_4{BrRSlEyxNlR)v1hFFYofajBY#WH-&Q8^f92m_iqTJHnj5tJy6ST6@%IAz z?*3VBts`S6?cox;VT}k|k;l`D0JyuXDx7u2CM<{SQ|>!)0I&L0P1&nH&n@@k)JpaG zG_!39$->Y8zy+~Uc?@GoK)DmnEeW9We>p1uBQnCm+8bL6^)Kfm0g3eYrN(9xCZbOU zkgUCNArvizs#%bigIaB)eH z#gGq6*~QL`7MP3M0riXtj$G=HVlK7PG^H6y^rMSl$l-~3^(1!$M1nDSKdvRkvJl`@ zGVe-J5@$vGd?z(beZ9KQOM&L3GrL5_ng=iOg2Jl-(bG@82H?)pz>UY040u4>S#u6g zg8{#7dEkZ*GWT?j4S0tz8Q-ly+3W7X1@NL6+1*qm@g85^Q<6ObniFh1!Vd1AY818w z>iLTmLN>R?w?@vh{D>OYmx58gKUGF)^{D;K4HhU}kjG&-kk{IRaa85|fT$DIdKC`v zZ=8%c_~#%eqvW^Z79Q~Sxh>Ag>X-b(mpiX%c9bQC8CFpHmNGkRJOr*dHw=#}f%KuB zl(E23|4xU<+0N`rH#9Z+(%iU&bTuhzI-?j%?21H5S%0@ZpNfO3uw|D-hG!7 zkx;XYMy^2eS)E!~1%Gz2L6GYCF&aHs&b6RibI$SwjauQ*t{Zu(*kFKBH|8{u-4N(p z=E|lAWp+HJITt&$OAdg2gKdp?yUyiF>T;ug=IoO_#i0i9c-nw}(*Uiw(#&v%&K$}Z zcXlajlG`b7*Guc#p5!fGF(xM`mt+7}=vtbw&b3+Pz|`a|Ey>LH(>&b=;Nu$`_T5v>mcxJDS#gmwRTc!@OoB=!&1t4B zS-UufY0afG>A97H4x^C`1A$FKsMom*EFs9cmd3*(vt6+IZXp4YR?h(i)yvJ$DoDt4 z_EcF>ST(LaRPHkez{MIPJ)gje$qxaPyR!QY__`{jN zuw!pnLi-aRF^8RKYUc(tx*szcy;Hs_At53B?@8mL$FIZ>Z3NBPuPkDUqx+Qbv@n+w z7e;T*urFcY=d8>#>^Np(TBQL@DTzm4gC5g{gmB1oV(9H-`RVAXMZQr zI~?rQRX9;P@Om;4=PCMva&A zOmbPQ=HXtU@To#1>VHo zLa^!0oKQUc)v0TXW`&b~Q4oBUiU;tvo_9vL^7^$$df-;{>x2aH_Gi&R@|hXv;;ioY zl-EyZ`F|^2mDha{XB(XYTQz3(h33{MN3eBB2LSKA#CN5e5g(L8#lYKNS4Wl_-E{)M z|DAyjdDtBlzkd4u$S~r4v#1!xxgVZ?GYh%T!5sG=i5S3GU_J}q8@0y`$Yu?ZfS>M6 zLYeN+V<{2n@1KD#TT$rs7Ymsi;Nv<(;?{lO{^@MwTUot3ElGfz(pdH`BE)*RXW0Y) z1+OU~#a2061BSs2^kkR1#U`(x&i4PuG{t%~|7Z_lFTGW$2uG#s0TADr_bygK+Ryh` zV!7u@Go|eO5%cCKhP(OkE9h|XsTav_MDTqShqRlWEwRuVy9V`H$Q$qPDcVKG0!Tl2 zChl(?5gp71sU~Q#ohfC0A)NP!47N#~h5$Dz{PxQO)|NgW)Gn7rMcqcI`@Xt8`~aN3 z?Y1&UP@lNpfI4STG%oqP2P(5oqmU(G&xi?2`Oy5DkI0^>6$N54bvsmscgSZ?r&6o- z;4@;bVWA(1u65xj+;9qQmnM_Jh#~fvSYJVA?Vykw4{&z)9?Vf(SJjF|Ee7Fvl)06^ zV#dVe&?S6G2Q{fh@*2fw!&b7WHpCZKX}Jkn-$keLYsNENuxe)B1MDx68nCfPChGGu zoRkyId4$Dz=tRwjgv1;U`z$&)wtOBX@D94lMqPf!0@S$Q59LfCtQ=p;&q!7pZ-Le? zK|yP!z{hk^TZS2rKtLCVRIqAPF*?P2317lf4(cO=O8?$^deF zJO%&Ca1MP$2gPY>$Rp680XBd7lsL+;HbpAGZ{Gkn;u-P4a=v6SI+Ob|QV}&7%I~>I zLSGF}X)yiuP&eJqb&K;ImmN5f7IAx0R6(aUq=_8KiwKV1ig592YY7z0mu#%+i}?u` zoJ{LC;t|X_V~T>|<{IG_WL)7s4jkmC`I3=DGd>)-gFHFeWgR&pOSdiO&Eq+HL`snLqu-t?)l8fl$Pl9+tU0V*?0Gp0IvrWy;4ogz$DN8B2gOJ8lvzqPEykvpl_U zoWnqj%J?_W(c(3kwREa~oB|_Xz`T%#Rz<%RHpVG`y0ma@xhwgnUPPBKK%*w{>VZB# zr0_BbIe9dOsK>B|+^{hZuA`Q#@s!JZ(L8jL=nfTV=I_pDo(a1nPhnbUomF7G<}$DRv=rch9;hzvp|3^^(PXtXM90Rw z2;Zl9oy{G0mKP>(i?d3FVDqbpi}ElWwE)qcvelY6+@(y9FjyomYCKwpKI;)P*Q3^9B^$3}>! ze5Tyn=n}qG`2&~VDCChe1;)k4_1-}OZ9&qijz@o)Zpe-EjMTWM5;K^^`%LBpgp+#0%r(4@5PG7pfYWl6i!F5S%Y@ zORR6Y*eRw>T$}W*qD|#^nU!{!PY#G8LwbT(zD2#^!WNnKsfg02Ys33k!G@xv;gG9PrQvis)#@BMq^V>yd?=H9<%hPB!G06$g<9k7%(4^`9cdKo zqPF0i3}#*HY93**SK$knl?vc%A6`g;v7~$!fqhTOH$fQ7L9MZ*ef3)mmKty!E{)NdLj0RPX=9 z;h#ACOr&i2?Do_K5u4hH6mdrG_f~?eWTsnE>c}&s-4Rb(LLY z!XJOJ0c3c2sU&)8sDnTsF?`6k>2BeT{1Sl`5Fcnke;(TYRausPY9Gu)?sHy5XVwF| z`un95Yom45K&1%>60CFICm4>{H^&HQiL`l-ni}q&i*2O?Ehl$q`^}EPrA4~|Bhm2@ z&a_g1-`zI;t)^R3!4m=)^h$F*zI;vSQY@UU-&$6fSCC(<&}Cf}9GokNS8FLg}nJ5n%b1#Y;?mp3<(;ONtcMLVFwvQh=LeK2IR5H}! zIcCvQ;wRryJP96tQtX^AjWDoC$Iv`n6E2?=s!^%bD2r!BcFc==<#R#z&R(PPf@7U} z&7lQ%sx?K9sdByR&qm^{WdjQu;!-?mqNCxW$o7DW#Ni-Awb8Xz%|XEb<7v8!50%Qa=aQS+=B8m7a<7$*MEHn&paN~(vT-A~ zT_d-0Hq(lH&`S&fPcN&@Bv*r0MJ3Z7Cq*+PRP=(WE4!=P%)R%Io#_wez~Oh!e1G5X z=XFl{mqB*{)E=w#YvDw-P1w z%B)p-HQYfTzIonSpsdZDzpwwOr*iJM>%MTI$9SBMGHVGdj*InAzC=U2#a-Zu0XU>B zWS{`JVyD0RKatkJHGqr&`z_r83(GbIlV|e*xM?yKtl&aOU}67%TBO^#tXmjLtuHQ@ zw7r4-;(cLaQ?E=QQ5Vv5w9A4Sa9up<;H?E6N})0a*Redi$Z5A7^3SStz}Ng>ibPR_ zyDf}%la)MBIRk)I0po71g&^;k2^!Xnbb>E|7QQkEv$yYi-w{@lr((ee_5#9{6f(@A zo%))My35bw^>hRRP~IU!g;t%RoV2_p5KHbOY3~<^yt6&>hSwKqdty zRK7>_KKmzwohLf*Pt`1~r8|z@D$Qe9hj%7J{iBHQ_OiiVEN8xDHszEzMTIqFMYpP1 zUoE)ts}?!jEupJ`LtqVk^$1;ugsWuJ_tIUg^durHvMU){!ZrGHtg2O9DpD*1rRk&k zmdf5XqIScK^Nbvc%Dz@aJ!b|I-R#|2M{^WT0Ii48exJwj+bl7-qxs8rAgx_r zTo{(D_7P% zaw4F%PGZkK-bA@pD7~S6s-ZPKJT3|-KJD8-*Ohm{J1wQp5fStv%5omgo^6^DIg|E^ z8g2S8yCciez4^&}8VcQ0Xqq1yL(*4XS!>nlWdl||wL1T*{~xPZ_e=j~A)0J(kU#a% zv7H&p0~xoTIhAhyE!8FbC^V8)3}Q!VeJnG2ucsjR4o}jRq|IFwG!bfP~HP9K&(r zntvCb#)m3cG*-*TgZc_R&kHH_7ZAYt{xGCluv)W@$9oh*gER^je;nH&QAuIzRilL; zkU+-9zK>uqAnvaLhSw?VYY8*}xC$-u;uqTxz?NR6SIR`!-;t*68WnTy6Qfvkn7;LEPH*8;%V_C6xBV!0_OP4sW0w=v1Z>{fgcf@P^Tla-{q z1M*Uxz+&xG&^f1G7FRa_@uC@>`a8yC=qu+QG6?+;Z*L2dKe)C?vn=U+6M)`T3B5s7 z1Bm^NBO#>ba~Ua$K}iK%X(>Czle#U$5|dbSmf6TQ*B6{IQ@>)ZEVw)^;LJTFt1H9B z@H_PJAU^96k`-HNM+lK-dOFs)1Bb67$bnw5$!>=44>%n*J{5S&%i+4icCXcxLnY>+0yOdmh%A4i%pNo4+Wj3fvSbB ze6(J%L4_M1ea~>5%|}-lj)L!Udi=gSK5cb40xGTQ^Cg(4{#`3~;h@5CFLH=1d6g5v zK5fNf5HsnyO~KquQ{Z#^I%;V4vF{7C!_kXZl3bN3zpx%|hsGL?ARYC&UL*`&)V zk>|S;=NS~HTy{NWmt4|Tr&0)|a+0b~Of1O>(_DcYdPQ4_#FMC)b2{u@VOl?|8SoaQ zD3()vUv-7|WpkV#U?Mct+76vq>a#&0N=v8v^)>ypxhD~ZpC!IlCMDozXq*G(-^9}F zs@ih>74ZF|FYjyK^y_aZftKFV1UK}ybv zg#F@#fnwk1oGqHV-mNf*vASzfpqcgZeD$7UIYPa0r&z-+%Q=WYHJHhPvs|W@hG@EN zp)V_R&IJOAM_bvsNt)Brh@bV>wV25ml0}z`)+(ZU;-qD@@u&2`ZZWO`bCZ#}$Em$+ zaF%E<(W7g?-vdtBUMOccQnUBAUn*BG6=_6@d0qXeN4Z^FQhA!Z&O|dGAHk91x0u%G zgONugCB*$(m*z(S=u>s$EYVcA2uD~kWf_OyM{g{?ovkkP>+Vg5D=MWN!{f8G(5a-h zdye%HN|pcy&5MU9s2UTX$2FY^JoUeSEuforU_JxL0Zv51s4KD2%*sq5)chbBp5O;D3+9?qct^C^TVpVy0>oLtM}dWopZkL zzBEtw^(sp9l>h)L8#lOk1AxRxF9msc42-rw2#~6X0wk;d(oG$;R~EJ4vCRTukn=AS zZ^AOdB;&Z5pTw1wy|)OPfvxTobr=2|vEnI*(qG@Jx$xhGNg~eeMydB>9>%SmwSg5h zL-HvZfc{nSb2}|I9N9@nlH5K(QLc3c}@d>?8r`${lIgZ;{U!DvZ;{lZ!B z7WiYaW5xii+nw?`==+--e5(MY_2Eqe$mUCiK<~Gi$Zv0tzV)@)5o2-&v;lqTDxAWs zRF<&V{&WRE%F3IC(#Kp7S+}&Afex3M8>n?mn}In%KIDDmqgEGE}3##(*MYDm1q}D;oQglO^Y%+^bLv z4di?K$yMlO2j#Y~V)jyKE8MpB?hfSI7@_BBp{|{IN<|hIzDp$9xyv2dQT{=14zFH^ z9snJiq$F{pESESOSy0E=E(1)qP`RqRbW`PY15eLXhy@?-s-vvJf-0 zqb4_}N^gVaY(;^ZOCL;ungy>%m{6e~a}^P*yrr6KP%XiLh=o+HB0P32g8zaa`9TBo zYB4-U!eeUY(wsX;?lPou(KGLbpjH7D`;fDgC8{pfW%kSJ)I-A;ZjSM2Zb|uxh;(Jt=4BLHjQQoE) zbr>xy<0@+^fUXJ36i^D+>rwsg!+^J#YSeF2nOLaGjEhs#N$rYXI~f8^#2S$BRt+hn z1ZVTudD~0;((g1mH#jT85$5c$-tzca_O6>V#$?%%KrU~-qv;R&G%>HvhvAIF^X05= zNDbm5zKY}W#8w&FX^QdekIy%`3T_)wxxh);{IZ<7XY{SX9e2HwJpvd%^ZOVc-OhZW zeb8i#RUtqK2A$POsIUClJ~+j+h?kf6}y|54RUQv2H*vu@zg#NJ59kcw+>w zq+z$0QP8y0m}0ET9$YkTd@YX6Xg}^Q^^!jSaH8G18Mm9RdOo@s*LD{K;M|veR}o)j zzwjXIgZ3}j4Q!M7yen&EK>f)XFZmY3hQr9oZdCdgW=(P0+inN*6m{C5>oe01nERG{VqbNJWWVzA4i&I3=8$tG&)|3%-3HgN-}iRzukRUmII_E^w*GL-jnEyP3^6%GBF8(-W)Af4QJDLCVpW@sM4?iR zL7WH=6j;+mM4=aYn-?x|Qm_bF*ot$*G@MdE6&vD{w&J#$E%Aw_!4rjrvBvyW)?P5pD8vp?O_JG@>D)l z-7lnA%8hia-zHVFZ>{YgMfE99M7snkgWIDyFv4Hef-$ed% zR)u}_=@-3=RQane#V4wQDEWi(tXPY_O&A38vSP)-Wi%v7cW!qG8ZyW$)dr&*A%hdr z(G4<4myWn_B3t6=bYtG1N@D(3X(jL(_K4uXj#G>manUcX9 z-q=ft=H-k`5Y>xGrnOO;S4g53+Bu7D1lnNIp$+K8DiDbV!;&ps^Gl zvo52={P89JB~95@U5OQP{HIko{l3Pcsw!Yb=9-p4VW&8P(EY>EjzSsgd+>?jSaenEwxe`ek7Od>@HrQ30AsN6(nsJEN%E$M|Y!UlBo#6*hcqVqj?_r*C5O zeBgejhgtXBY!TnEmrl8cuZ;xz~~&H4y_l+sFLl z3rZa0Uy=!ik@Uy}lfWbpKDVgcBPK|x`;uBd;Mz`$UpPza hAD?CaSKl?Jl(QvI8Cd(5E8(v-V56(MOXV6U<)4seU~d2b literal 0 HcmV?d00001 diff --git a/test/constraint/equal_line_arc_len/normal.slvs b/test/constraint/equal_line_arc_len/normal.slvs new file mode 100644 index 0000000..1f7869e --- /dev/null +++ b/test/constraint/equal_line_arc_len/normal.slvs @@ -0,0 +1,367 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-4.99993226121819223096 +AddParam + +Param.h.v.=00040011 +Param.val=10.00006773878180865722 +AddParam + +Param.h.v.=00040013 +Param.val=-9.99975209012342425297 +AddParam + +Param.h.v.=00040014 +Param.val=9.99968435134161737210 +AddParam + +Param.h.v.=00040016 +Param.val=-5.00031564865838440426 +AddParam + +Param.h.v.=00040017 +Param.val=5.00024790987657663521 +AddParam + +Param.h.v.=00050010 +Param.val=4.99990506249655908277 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=12.85283693258537773829 +AddParam + +Param.h.v.=00050014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=500 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=14000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.point[2].v=00040003 +Entity.normal.v=00040020 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-4.99993226121819223096 +Entity.actPoint.y=10.00006773878180865722 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-9.99975209012342425297 +Entity.actPoint.y=9.99968435134161737210 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040003 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00031564865838440426 +Entity.actPoint.y=5.00024790987657663521 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=4.99990506249655908277 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=12.85283693258537773829 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=55 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00050000 +Constraint.entityB.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/equal_line_arc_len/normal_v20.slvs b/test/constraint/equal_line_arc_len/normal_v20.slvs new file mode 100644 index 0000000..9f4b27b --- /dev/null +++ b/test/constraint/equal_line_arc_len/normal_v20.slvs @@ -0,0 +1,367 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-4.99993226121819220000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00006773878180900000 +AddParam + +Param.h.v.=00040013 +Param.val=-9.99975209012342430000 +AddParam + +Param.h.v.=00040014 +Param.val=9.99968435134161740000 +AddParam + +Param.h.v.=00040016 +Param.val=-5.00031564865838440000 +AddParam + +Param.h.v.=00040017 +Param.val=5.00024790987657660000 +AddParam + +Param.h.v.=00050010 +Param.val=4.99990506249655910000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=12.85283693258537800000 +AddParam + +Param.h.v.=00050014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=500 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=14000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.point[2].v=00040003 +Entity.normal.v=00040020 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-4.99993226121819220000 +Entity.actPoint.y=10.00006773878180900000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-9.99975209012342430000 +Entity.actPoint.y=9.99968435134161740000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040003 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00031564865838440000 +Entity.actPoint.y=5.00024790987657660000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=4.99990506249655910000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=12.85283693258537800000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=55 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00050000 +Constraint.entityB.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/equal_line_arc_len/normal_v22.slvs b/test/constraint/equal_line_arc_len/normal_v22.slvs new file mode 100644 index 0000000..733864b --- /dev/null +++ b/test/constraint/equal_line_arc_len/normal_v22.slvs @@ -0,0 +1,367 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-4.99993226121819220000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00006773878180900000 +AddParam + +Param.h.v.=00040013 +Param.val=-9.99975209012342430000 +AddParam + +Param.h.v.=00040014 +Param.val=9.99968435134161740000 +AddParam + +Param.h.v.=00040016 +Param.val=-5.00031564865838440000 +AddParam + +Param.h.v.=00040017 +Param.val=5.00024790987657660000 +AddParam + +Param.h.v.=00050010 +Param.val=4.99990506249655910000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=12.85283693258537800000 +AddParam + +Param.h.v.=00050014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=500 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=14000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.point[2].v=00040003 +Entity.normal.v=00040020 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-4.99993226121819220000 +Entity.actPoint.y=10.00006773878180900000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-9.99975209012342430000 +Entity.actPoint.y=9.99968435134161740000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040003 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00031564865838440000 +Entity.actPoint.y=5.00024790987657660000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=4.99990506249655910000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=12.85283693258537800000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=55 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00050000 +Constraint.entityB.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/equal_line_arc_len/pi.png b/test/constraint/equal_line_arc_len/pi.png new file mode 100644 index 0000000000000000000000000000000000000000..4e06218a09172def33473526f2b172fe806363ae GIT binary patch literal 4667 zcmeHLX;f3!7CyNofJ6pCCK;kwp_GWIb*M5AA|h6)VnB>nWs;yEh{}8uQHw>8Vj08% z^$Aj@t4NtjNE|AP2vif0xo8P8$PhxF#DsT);F8rx>-*6kZ>_GCtaI17``ml>xA*?` zcW&I)Eslx`3lsnVicTBtT>*e7(!abcHpBEuYXv|Rai1T16ZfAARw=Xy>l-_1PeFAI1oGMZ!(HLuNQWC*RGT*$@mX_ zbuql-V}TPNexzXI7u{vurK>aYJ8Hx3`4k;&JM&b>bRP6q`{{9PxChY_)Gw`>>CQJI z!O9Q-(|X`laxZO{f> zE9C)_lr}506wDGPfU(XQjKdRl)1)%r#K_s zny`G_z6=8JXNI%o91{H`M4$lN=1m*qnWFLIIK(D>3Lx0iVds;X!Jgsk#|d7a=$%8w z)n{DepZXAHsd0dR{_Y<&allMfZMJN;w(2emCxBdO&7dR>5gZ-iB`1m{w|0Pwu5I|5 znF3;K&k#&ZYAXsR)qias* zzwsKn^_9jPAN>bl>js!@V}DHnu~VT0fyTp2^1!$ent2X|w31m2=&k}B01++wO$uT; zIv>T=^8Ry$PIGg=`Igq6xQpM-(dd7�KG~d(fh9YWLe?bIH!JsM=vd5EUXGU048~ zy1+JRrI0mjbFLK1Ttz1=?@~aPK}6_2=kzq#LoV3dE-5yXcxxlC)qyY#W@p@22Zg#U9#uem@ftrndE$n?Wiie&vvvaM#hNrKZYZ9`P~2PyD0VP=12+G&0SYg9W&*gX z6qJrVk0wv1ut$3re&makbJ&|ql_4~R#nANB$HTcf#od9*$M4qZ;b9XsGR4Ldhn{>N zyrEc8Avk{eqexPNPKfB!E=-ZwbiRmhQi1LcVWQu^`Hvn(Pc(f2wme#5JMG96^@S*F zWFL8Z@PT;8MC(vle(~#$2X!^$En1Of6Ja&2%^j8kHV+!opg%CqkGoo!(OMB!-*fOY zi}C@O+WJZfU70G619vy0@b;|jz=Nk3$&2nDU3N=WfgTLR1NLlJbId=q212#}1V zC6>j#5OevSOq)f(e8kR-%`l2p2ZJgUlp#VvlkNLlKhE~H1wX02uLQSX5*Ih^E6S*e z1p$jtSmqlt;NB#XuSWxTt04vb9-Gfwg2Iba=it`49(jPlE1N|Dd5h3aMQk)*jNQVP zQ0GyG46#uk8&4G)r&L3o`gB5IaE=P%h}VcrHr4Lwx$w- zl(3B!_1#=i%XnGwuDi~-!wBt^$vP1H$ZYQvCo`1G6UbKyW~Ouk$>##Wj435W-mA?; z;1`S+Paev4w9*M$SsL3`0dKqmSdu^wqX~he&mIY zkQ>sf8z?Hm*6C9eg0QkudeeSl6G4-8YYw@m`g-==j*30B@kXRUP=&eKWjYZuEK`%v zePzG7t#@^ZwIdenOXKCsB@MFHdTM0E_F0wT8eP(p-%Dz=CN_p0OMLi^>z{Pe$+JLXX08l4g_^VEXwT0S{ z7gup$%!@%#8F3Uq$UPMbSY*{wSgq<2v~ZG~t?TQ?Caw}Lrm~G zLgjv5jAW-5`>i}VlQhvYSdmz`Oj}{;T4g$62$$s^Wcm%(F@s+XwsWe;Z^c#3+2WQe z@nF?NOH`dRt)~2?_}1^M=8MX@Eh2>$yJ{o@6J*h{wQHWsx;YxzWmXF+-D=*MlrrB* zhB=WnjibH1XLZoEGvO~EK1`GkvW@w2FTvLU#afKmrP3uolhVLhK_}?82)tQO4=pt- zX7I6E+neQvJp2;i<4iJ(9}gPfwc?!_EKMoS-KvZJTrs(6Quv-n{QzPy)vPi}h8-Uf zz+a25*~0|l?@tSpKb2^q^i_47ss7rrF*TpeO~t` zOVq<8ViGz59eDOz!CRBkeuwQ4bx{+gW3cq~ywPWQmE z*37r?wubf6a@4`76RdYmI=aAm@zOESpV>08mC4lVQUK$j?L1g-)M=!|N?)!txZbJ+ zlswg;$Vd=B(5Z!8<;P*&T9c=g$HkCUF+x4m+=2CZ*0{lKfT++z^Lq}D0~RGgol>Iq zFGsZ-zR6j|B8q6Za!KOE8>TDmX5_Nmsss@9JB41%yR|xN_GpUcW zl<7;4FzSp*^6pCFEwciC`2t-^Ny8nV1)(}S7+}1Mv_aY_&hCGQ@>;f*3vW3tzcaR? zBlKL<$giF2w;chzM6foAImj-*Ox6`75gU^Peo>8u zza{m`M;v)K$mB%~2sx76F&UPz?qXEm;n!`~59GEW;Tj%^)DVFPZ5$Rx?xr!_;rw3g z!3Coc%G^rPK1&Plrf~BZCv4TI4ka;i#cirX+j`oIk@MlykT?|tdRS^-$|qDMVKk{6UTj(8X-__FCEo9XC z^m*wFs_y;a%xi5Bw6QohBK literal 0 HcmV?d00001 diff --git a/test/constraint/equal_line_arc_len/pi.slvs b/test/constraint/equal_line_arc_len/pi.slvs new file mode 100644 index 0000000..009148c --- /dev/null +++ b/test/constraint/equal_line_arc_len/pi.slvs @@ -0,0 +1,367 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-4.99999651845403381145 +AddParam + +Param.h.v.=00040011 +Param.val=9.99999682365714548382 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000349810587607635 +AddParam + +Param.h.v.=00040014 +Param.val=14.99999363650495176614 +AddParam + +Param.h.v.=00040016 +Param.val=-5.00000000434181757214 +AddParam + +Param.h.v.=00040017 +Param.val=5.00000001080568345913 +AddParam + +Param.h.v.=00050010 +Param.val=4.99998746596540222242 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=20.70793025565613021399 +AddParam + +Param.h.v.=00050014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=500 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=14000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.point[2].v=00040003 +Entity.normal.v=00040020 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-4.99999651845403381145 +Entity.actPoint.y=9.99999682365714548382 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000349810587607635 +Entity.actPoint.y=14.99999363650495176614 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040003 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000434181757214 +Entity.actPoint.y=5.00000001080568345913 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=4.99998746596540222242 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=20.70793025565613021399 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=55 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00050000 +Constraint.entityB.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/equal_line_arc_len/tau.png b/test/constraint/equal_line_arc_len/tau.png new file mode 100644 index 0000000000000000000000000000000000000000..1abd363cd474e157522b2b20b5ebef35ccdd5194 GIT binary patch literal 4963 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 GM*I_>3NxJm literal 0 HcmV?d00001 diff --git a/test/constraint/equal_radius/normal.slvs b/test/constraint/equal_radius/normal.slvs new file mode 100644 index 0000000..978b12c --- /dev/null +++ b/test/constraint/equal_radius/normal.slvs @@ -0,0 +1,349 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050040 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.normal.v=00050020 +Entity.distance.v=00050040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=130 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/equal_radius/normal_v20.slvs b/test/constraint/equal_radius/normal_v20.slvs new file mode 100644 index 0000000..cd891bf --- /dev/null +++ b/test/constraint/equal_radius/normal_v20.slvs @@ -0,0 +1,347 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050040 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.normal.v=00050020 +Entity.distance.v=00050040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=130 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/equal_radius/normal_v22.slvs b/test/constraint/equal_radius/normal_v22.slvs new file mode 100644 index 0000000..57b6d9b --- /dev/null +++ b/test/constraint/equal_radius/normal_v22.slvs @@ -0,0 +1,349 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050040 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.normal.v=00050020 +Entity.distance.v=00050040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=130 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/equal_radius/test.cpp b/test/constraint/equal_radius/test.cpp new file mode 100644 index 0000000..5e9db1b --- /dev/null +++ b/test/constraint/equal_radius/test.cpp @@ -0,0 +1,17 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} diff --git a/test/constraint/horizontal/line.png b/test/constraint/horizontal/line.png new file mode 100644 index 0000000000000000000000000000000000000000..cd911e5a9c6f4423bb5b3d0b669208c6b0afba60 GIT binary patch literal 4274 zcmeHLdr(t%8vO_mfrv7qfD#^c*63C!s375Cu~ix^NCgCg0gYlo79s2)5Dbrmvb(aP zjO!LCvd~JQH3br(yh*rfOBBJl8UjO1fTB_eFM*H}2-yULY1WlGJF|c6jDKW)Gs!pi ze)oLmJLlYF9`+>|8*MTI0ONxP_WJ{%h?b22V1i=<+}1OLy?F=<3{2IxKKVZ?V%tPV9PeaC&knH0 z>gVV91$>3DmMbf#q)17sYaz!j$B($O?(|v6W5uy}Y_|2GPUBhwo!@FMt9Y;-#b7vx zx@bRyw^H$iD?moxYoE5|W`)=ty1bSO*j!}i&?>-vn!X4NJ;_^GlQGin9Hr!@K$p8jKBPP3TqureWxw6!oBdgRsTubLxu#&van(Bg}C6Qey ztU1zALNkTNxiV(%=plF#@ltVD)Oxi%AM30M&;{&0J&f(s2sxVm*n|l0ILNkFW|$Qm zSP)5*DSs8lzBkFxj;@;*ek;zDh}0{S;+y1BQ`*dI(Pm6xHiBw-A)@xAB&XazeCF1q z`Vu{e6Q=OKe_(FTt~6PoQj~K-CCS%VQ}zkfNVhbodc?hlPq5xsmAi-E7g1EjDWfPO zf}8ugA6gQ1Lsv6!ovi9^;fY(ap=?S|dv232>v&A6nMsGh(vz#qf=_;qFx*bXp)eLa zH;F(bFv@op+iMyN?LlLQOY}>F#+&ra_U7H#&>Ou8(tp@+xMLU=dFb(gW!F*48kkP;Pu3H zWJO20mM`zErts_yrneji5WcW=DZ912lLeu&9k?8OS-m)U7nb)nlOj``$rXkInC@EK$JcK_%W6B%Miao_ z+@X7a?FBUWo^3;z`MWFMlbxlMuZ78tE^KD$rLuRw(jUv8lDo%#St@PmVgx#ep0=0q zZLV&s1hPl}s-TW@R{~|KD_gz?GB!>xsDpJkKP%DmnLpBA#M%-gFb|dAk2#b-U2#+1Pw*~ORYjFdKb#3kNVC%EZLLQ|v9euL=m;-dWJ2|G`ppWt|?z~fs7c41%jdRkA@)W$*#P&bD)qowC7V}-RL zb(hPsrxTSH??19SY4Z=&cl;SYsf5%VWd((|T~X5F18z}HdxoO-ijB&%t>fgAv4WZG zuT0LIQVJ<*1i9VtV3kqadz`lmSk7 zp8aitxZ%>YIL$=TSe{E&ecc()trUtlS-TH4%DZ}I-IAx8_PqOIPWNH)i(|?oW8Qr) zR#T}lR;<6M%&}{Dw!Gw8gT`|{e4qymalEJw2e>r&bERkp>zoX8qRMyrL2-<~Z%8UP&dE?MG>TFVfw5jfbE` Q{a*we^zz+b@gX_=UqeK}y#N3J literal 0 HcmV?d00001 diff --git a/test/constraint/horizontal/line.slvs b/test/constraint/horizontal/line.slvs new file mode 100644 index 0000000..cebb650 --- /dev/null +++ b/test/constraint/horizontal/line.slvs @@ -0,0 +1,288 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=80 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/horizontal/line_v20.slvs b/test/constraint/horizontal/line_v20.slvs new file mode 100644 index 0000000..8909c0f --- /dev/null +++ b/test/constraint/horizontal/line_v20.slvs @@ -0,0 +1,286 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=80 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/horizontal/line_v22.slvs b/test/constraint/horizontal/line_v22.slvs new file mode 100644 index 0000000..8b2dd43 --- /dev/null +++ b/test/constraint/horizontal/line_v22.slvs @@ -0,0 +1,288 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=80 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/horizontal/pt_pt.png b/test/constraint/horizontal/pt_pt.png new file mode 100644 index 0000000000000000000000000000000000000000..7614ea815f7303cc0a08f2839bf9362fc9618dae GIT binary patch literal 4268 zcmeI0dr*^C8pclo;T|rfA|eYag?3l216xv+ixr_XL0T0G1VStV<(7b46cR$9s8uK| zYgev?>7pQ)KmS zJM8II{4+{himj<|b5sXZeZybd7KGxpkkmn|#pvrS>_6I4hqr+HZvA3T zB>B+Kccc_t=RQNr3!$H$+0qbKtp-qk)m)e7(O?p1(_SGQW?byodIs000J~VrN+!bb zh30;ufp$%*^gn8B!0(s5m1CJdoDvPF7R7j2_R8Z?5N`t4-Z5E2@w|99%P{IG9J9)2 zWnUqJ3n4g)8f6q*3c-8l1zR~-w(Heu+3Jzwogxiuyea#0@d(F`xJ}FEx^5<70f}iI z50bB`rEIybadwQcA`$bh$^9r}MbbEgW7fuvEmQXWU|ixdpQ`I7Et?Nd=ZA)`o{2d? z#3Pd&R7={(v_Zz2PD5k{MM~(HOidFlO+>;z^l4v|GAj(mvnXHCvvwN<$c<*PYP8D# zZv70QSh29FIH(gBgpWoHS<=Bx3=I(bf&Lo})Y@LbF4dx3Y!>9G@Zm?l<$g-aC=48L zYBdH`mNabtrHaotkfI?*OPT-6I@V7jLO>{<7IVJ_2J(()J&g|3gd(y^el&})VXM!7 z@iL1S#c?dx!m)PkaEl)AirgY4@=oko#(9SUWcvWT>fe*q$iFOETc6*-3=p zu^3;PfbsX_9jM4u#%FIkFI@R~D0xBeJZg!vsowIlu1)5Tssae}@@?{}7&f5we_0Ge z+VzS+PW>*#j$Z3w1mds6s2Kq$Q#rKByZCtxIxskzB3$mXrL_WJ74c)+y*Sp>JD{K; z#t{`@dV9U29@;OfnGGh5=qpv6@)aOWrH2`;0t{!7^>K!Q07{Y&4bEGG@wem+JG=(f zMw1o*@IraX@qzJBFvP}BUa(!vG|DwJ-xg5M$$96(bqFOpqN(UHka6-N1M#c}$&uN$`eD!g&bn%9gk`P|Ogr zUNghp3B>Elzx}BI0ZhLi&h)cv0#wwG!&{jm%yAgl3Flv~V)m{iF51OL& zp^lxmHq76bu?%zf``k33u`-J5G@E=>6K(m^ zG_8~CF@lx0GJAVCDT2f&>9GRWP*S@cP+piGxAoS+Q&9HLur=&Ryy(Txki06Lw!YEoZ#;dFDy)Xko+(})>%=C?>J zi@A1ThUgu)=n)eCBuxdfEVH1G=3Hv_?61VB4|shg8V!3wG0WQ{E69)GI&04juCA!q ze~}RSsffq_xP$b~!sF!?OC%_?C*a$s_ge6A1L+ZV`E6(y@r{ET?w=+;*hL=jV)k`N z?QW8oqSpi!OKrU$@zUB0`>LYEW>ReES#dej%d$KJq&ea0>_i=}n{laUr+JIye-S$; z;&o2kIhjL8bCz@f>hdTyTbwnyJFMO1YGRk9Mo6*b)e~2XhkS#bBPf)5#kH3(LcdJO zYZ`BhNNv>xwofmQsYoR!+N_svIO_j?i=SgZK4$aI)?@HB`CAC!eejS+!@Kybe*;=Q Bs4D;f literal 0 HcmV?d00001 diff --git a/test/constraint/horizontal/pt_pt.slvs b/test/constraint/horizontal/pt_pt.slvs new file mode 100644 index 0000000..ac91222 --- /dev/null +++ b/test/constraint/horizontal/pt_pt.slvs @@ -0,0 +1,287 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=80 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.ptB.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/horizontal/pt_pt_v20.slvs b/test/constraint/horizontal/pt_pt_v20.slvs new file mode 100644 index 0000000..6a38678 --- /dev/null +++ b/test/constraint/horizontal/pt_pt_v20.slvs @@ -0,0 +1,287 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=80 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.ptB.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/horizontal/pt_pt_v22.slvs b/test/constraint/horizontal/pt_pt_v22.slvs new file mode 100644 index 0000000..cadcdba --- /dev/null +++ b/test/constraint/horizontal/pt_pt_v22.slvs @@ -0,0 +1,287 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=80 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.ptB.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/horizontal/test.cpp b/test/constraint/horizontal/test.cpp new file mode 100644 index 0000000..c08b5e1 --- /dev/null +++ b/test/constraint/horizontal/test.cpp @@ -0,0 +1,33 @@ +#include "harness.h" + +TEST_CASE(line_roundtrip) { + CHECK_LOAD("line.slvs"); + CHECK_RENDER("line.png"); + CHECK_SAVE("line.slvs"); +} + +TEST_CASE(line_migrate_from_v20) { + CHECK_LOAD("line_v20.slvs"); + CHECK_SAVE("line.slvs"); +} + +TEST_CASE(line_migrate_from_v22) { + CHECK_LOAD("line_v22.slvs"); + CHECK_SAVE("line.slvs"); +} + +TEST_CASE(pt_pt_roundtrip) { + CHECK_LOAD("pt_pt.slvs"); + CHECK_RENDER("pt_pt.png"); + CHECK_SAVE("pt_pt.slvs"); +} + +TEST_CASE(pt_pt_migrate_from_v20) { + CHECK_LOAD("pt_pt_v20.slvs"); + CHECK_SAVE("pt_pt.slvs"); +} + +TEST_CASE(pt_pt_migrate_from_v22) { + CHECK_LOAD("pt_pt_v22.slvs"); + CHECK_SAVE("pt_pt.slvs"); +} diff --git a/test/constraint/length_difference/normal.png b/test/constraint/length_difference/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..eec393188526e5735a171e5812ce60e1106f9dc7 GIT binary patch literal 4462 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

|{q0O&uARKPU{t zqiKRpq5n}*kYqyxv7n1;R%4sp;5?YqA6md79Mk7r5tanLjZ#!274((Vf_LB#IEh>Qq(TVdXd=eyZ-@ISi zZbPd^k~c!5evZiHUVn4?q41SJ>>!JtDr`E?c#Gqc8=pxPhDnWWcg^=h&R6ARNwIP+ zOLa#9g0=aYnbgH?wi-nw6 zkSMxwm_wIGHk_eiIg!h-G2qBddWKu5ueRW3$x}Q&vVN!lms7K$rnJBTlfIv*4vcqo zE-Uu7?@hWVN_<)4ue$v@dbzNpLFp`t`K@quuf$(9Ku}RT*99IizmBrLyYpx@jCXh> mnRkCqHJ{Mzzvmr?0=YG>rCrUr)(Lty1GYGBcewivF7_V?&t7)` literal 0 HcmV?d00001 diff --git a/test/constraint/length_ratio/normal.slvs b/test/constraint/length_ratio/normal.slvs new file mode 100644 index 0000000..0286fc1 --- /dev/null +++ b/test/constraint/length_ratio/normal.slvs @@ -0,0 +1,342 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=15.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=51 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=0.50000000000000000000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +Constraint.disp.offset.x=-10.00000000000000000000 +Constraint.disp.offset.y=5.00000000000000000000 +AddConstraint + diff --git a/test/constraint/length_ratio/normal_v20.slvs b/test/constraint/length_ratio/normal_v20.slvs new file mode 100644 index 0000000..a73f06b --- /dev/null +++ b/test/constraint/length_ratio/normal_v20.slvs @@ -0,0 +1,340 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=15.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=51 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=0.50000000000000000000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +Constraint.disp.offset.x=-10.00000000000000000000 +Constraint.disp.offset.y=5.00000000000000000000 +AddConstraint + diff --git a/test/constraint/length_ratio/normal_v22.slvs b/test/constraint/length_ratio/normal_v22.slvs new file mode 100644 index 0000000..08957de --- /dev/null +++ b/test/constraint/length_ratio/normal_v22.slvs @@ -0,0 +1,342 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=15.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=51 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=0.50000000000000000000 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +Constraint.disp.offset.x=-10.00000000000000000000 +Constraint.disp.offset.y=5.00000000000000000000 +AddConstraint + diff --git a/test/constraint/length_ratio/reference.png b/test/constraint/length_ratio/reference.png new file mode 100644 index 0000000000000000000000000000000000000000..5a5bfc9a074c0908f4d9d6581f78e97d724d5d5a GIT binary patch literal 4616 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 ftn_=l(o->*!kW$ literal 0 HcmV?d00001 diff --git a/test/constraint/parallel/free_in_3d.slvs b/test/constraint/parallel/free_in_3d.slvs new file mode 100644 index 0000000..1a054ed --- /dev/null +++ b/test/constraint/parallel/free_in_3d.slvs @@ -0,0 +1,295 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040015 +AddParam + +Param.h.v.=40000001 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=121 +Constraint.group.v=00000002 +Constraint.valP.v=40000001 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00030020 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/parallel/free_in_3d_v20.slvs b/test/constraint/parallel/free_in_3d_v20.slvs new file mode 100644 index 0000000..421eb11 --- /dev/null +++ b/test/constraint/parallel/free_in_3d_v20.slvs @@ -0,0 +1,290 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040015 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=121 +Constraint.group.v=00000002 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00030020 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/parallel/free_in_3d_v22.slvs b/test/constraint/parallel/free_in_3d_v22.slvs new file mode 100644 index 0000000..1628203 --- /dev/null +++ b/test/constraint/parallel/free_in_3d_v22.slvs @@ -0,0 +1,290 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040015 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=121 +Constraint.group.v=00000002 +Constraint.entityA.v=00040000 +Constraint.entityB.v=00030020 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/parallel/normal.png b/test/constraint/parallel/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..599c82bd97bf2d94813ad1045166c7d474efa1b8 GIT binary patch literal 4686 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 KtvPucA^tyPa1uHI literal 0 HcmV?d00001 diff --git a/test/constraint/perpendicular/normal.slvs b/test/constraint/perpendicular/normal.slvs new file mode 100644 index 0000000..a0d5930 --- /dev/null +++ b/test/constraint/perpendicular/normal.slvs @@ -0,0 +1,339 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=122 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00050000 +Constraint.entityB.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/perpendicular/normal_v20.slvs b/test/constraint/perpendicular/normal_v20.slvs new file mode 100644 index 0000000..905594d --- /dev/null +++ b/test/constraint/perpendicular/normal_v20.slvs @@ -0,0 +1,337 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=122 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00050000 +Constraint.entityB.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/perpendicular/normal_v22.slvs b/test/constraint/perpendicular/normal_v22.slvs new file mode 100644 index 0000000..342a049 --- /dev/null +++ b/test/constraint/perpendicular/normal_v22.slvs @@ -0,0 +1,339 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=122 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00050000 +Constraint.entityB.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/perpendicular/test.cpp b/test/constraint/perpendicular/test.cpp new file mode 100644 index 0000000..5e9db1b --- /dev/null +++ b/test/constraint/perpendicular/test.cpp @@ -0,0 +1,17 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} diff --git a/test/constraint/points_coincident/free_in_3d.png b/test/constraint/points_coincident/free_in_3d.png new file mode 100644 index 0000000000000000000000000000000000000000..df5a660e0e81c60c9c9b3da6edf17cc0009e979b GIT binary patch literal 4241 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 NJFM-k3bsHoe*hyMKyv^9 literal 0 HcmV?d00001 diff --git a/test/constraint/proj_pt_distance/normal.slvs b/test/constraint/proj_pt_distance/normal.slvs new file mode 100644 index 0000000..1658c3e --- /dev/null +++ b/test/constraint/proj_pt_distance/normal.slvs @@ -0,0 +1,290 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=10.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=34 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=5.00000000000000000000 +Constraint.ptA.v=00040000 +Constraint.ptB.v=00050000 +Constraint.entityA.v=00020020 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +Constraint.disp.offset.y=-4.00000000000000000000 +AddConstraint + diff --git a/test/constraint/proj_pt_distance/normal_v20.slvs b/test/constraint/proj_pt_distance/normal_v20.slvs new file mode 100644 index 0000000..7dd3084 --- /dev/null +++ b/test/constraint/proj_pt_distance/normal_v20.slvs @@ -0,0 +1,293 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=10.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=34 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=5.00000000000000000000 +Constraint.ptA.v=00040000 +Constraint.ptB.v=00050000 +Constraint.entityA.v=00020020 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +Constraint.disp.offset.x=0.00000000000000000000 +Constraint.disp.offset.y=-4.00000000000000000000 +AddConstraint + diff --git a/test/constraint/proj_pt_distance/normal_v22.slvs b/test/constraint/proj_pt_distance/normal_v22.slvs new file mode 100644 index 0000000..01ba48e --- /dev/null +++ b/test/constraint/proj_pt_distance/normal_v22.slvs @@ -0,0 +1,290 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=10.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=34 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=5.00000000000000000000 +Constraint.ptA.v=00040000 +Constraint.ptB.v=00050000 +Constraint.entityA.v=00020020 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +Constraint.disp.offset.y=-4.00000000000000000000 +AddConstraint + diff --git a/test/constraint/proj_pt_distance/reference.png b/test/constraint/proj_pt_distance/reference.png new file mode 100644 index 0000000000000000000000000000000000000000..571927b419b114e7171ffb4e9fee859ca0e13dac GIT binary patch literal 4566 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 DxI9+) literal 0 HcmV?d00001 diff --git a/test/constraint/proj_pt_distance/reference.slvs b/test/constraint/proj_pt_distance/reference.slvs new file mode 100644 index 0000000..d1203b9 --- /dev/null +++ b/test/constraint/proj_pt_distance/reference.slvs @@ -0,0 +1,290 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=10.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=34 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=5.00000000000000000000 +Constraint.ptA.v=00040000 +Constraint.ptB.v=00050000 +Constraint.entityA.v=00020020 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=1 +Constraint.disp.offset.y=-4.00000000000000000000 +AddConstraint + diff --git a/test/constraint/proj_pt_distance/reference_v20.slvs b/test/constraint/proj_pt_distance/reference_v20.slvs new file mode 100644 index 0000000..d5f3860 --- /dev/null +++ b/test/constraint/proj_pt_distance/reference_v20.slvs @@ -0,0 +1,292 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=10.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=34 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=5.00000000000000000000 +Constraint.ptA.v=00040000 +Constraint.ptB.v=00050000 +Constraint.entityA.v=00020020 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=1 +Constraint.disp.offset.y=-4.00000000000000000000 +AddConstraint + diff --git a/test/constraint/proj_pt_distance/reference_v22.slvs b/test/constraint/proj_pt_distance/reference_v22.slvs new file mode 100644 index 0000000..6461182 --- /dev/null +++ b/test/constraint/proj_pt_distance/reference_v22.slvs @@ -0,0 +1,290 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=10.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=34 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=5.00000000000000000000 +Constraint.ptA.v=00040000 +Constraint.ptB.v=00050000 +Constraint.entityA.v=00020020 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=1 +Constraint.disp.offset.y=-4.00000000000000000000 +AddConstraint + diff --git a/test/constraint/proj_pt_distance/test.cpp b/test/constraint/proj_pt_distance/test.cpp new file mode 100644 index 0000000..7e2a06e --- /dev/null +++ b/test/constraint/proj_pt_distance/test.cpp @@ -0,0 +1,33 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(reference_roundtrip) { + CHECK_LOAD("reference.slvs"); + CHECK_RENDER("reference.png"); + CHECK_SAVE("reference.slvs"); +} + +TEST_CASE(reference_migrate_from_v20) { + CHECK_LOAD("reference_v20.slvs"); + CHECK_SAVE("reference.slvs"); +} + +TEST_CASE(reference_migrate_from_v22) { + CHECK_LOAD("reference_v22.slvs"); + CHECK_SAVE("reference.slvs"); +} diff --git a/test/constraint/pt_face_distance/normal.png b/test/constraint/pt_face_distance/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..5fe42722bbb52363aa3c4bcff7a982c1ca233260 GIT binary patch literal 4314 zcmeI0dr(t%7RP^yNexDnH)s*CBNQY+6d$;Vs61TCLn{hdfpAfg-H8DN5rF~;Dhz@$ z-BpN{V#Nmvgv3X9L?S^{tOgO0%OeobEXL)jrUW%4>;<;zTurSz)1CdNGsz@#^GohI z=lA`6&*vn2OTZ@GISc0i0Cau+?70;HEM@9R_zbyna}TQ<0KKz5o*T9uEFA9H5NGbS zF!QNMCp<&<_?lTau@5>$i!Od+UF>qp^I*f@?K`5ZM48SE=66Syct67^b_zZ!zRi>^A(b{=~ocs)vjAbz|4e1 z34^|mYc>MVdK+zV}a(DfuG#JDhd{BYs+pi}Eq6!pb zN-2t(1^n*gwA7BVubehEhtXp6eh^4j+nulgve6p3rtIY7k(a3$p$|F(WS3aMEfRlJ zbVxChigW?l&QROd-Mv!LW)Hjr_xI?bN;L*|1qld%af*dX>iu08y5wI-#)B_E5D7%} zN?DCZ)iQzEpR@`H&--apj#(o=hy%?0TM|4_H-tlIA!AMbExwUAt!DG!RIGom7pt`7*)`DZ(x433>B?H88j4GR_i65DrV6 zryKVT2XwruK`6!sLj!{394MM2y!|q*%F1Mc;ob{9##Cn^n$n2 zJonN95PL$Hv0<&T$B&#fvUqTi6>=EqLJ5YX`3vwxPse-HIaI9hYmG;WY!V^cGwV`I zQCFvR+Ia{+_l^Y;&@YP}HbS;_Dn-bc0XRNo0?&CQ>k?zMN6_QK^I@1b^plXaH*U5! zgm$0V$FLq(W)sbrh#8PM#jBra+Tx!1Cgc<0=_+2b4%pP@&t1FQ2(V5}N3ZFjo&TIS z_s$`2roI+$MQ)b)%x6SMKwOg5dUYFzVUM^NtCcVD%zwm>_Nr53iHs!n;G_r9@g9cKYeqVEd7rrz4d^jd_+KH1^oKN3J^l>}o3}UfAu&w(VP{ z3q-!5U!NZdI}D;?b7I3YtG!<2TOGlfka0|&lLyYkC@x3uzhD?SUzv8o6y5I1h~>p8 z#Q-=pX=~|7t6|fA{2RLG&9w7A7X3!eHbI(-f6)3m-1VOvBssxYj@ zSxnAVE9hee)g|Lp0j|SA)BBQXP5%18&XtY4;46Nl|e8FFgLXI*bDa8&jd=Rl~0a`LA!QuV-R>746UI2)0Yu)&da4=;4 zV5?S~2!lF(F~0(bA5qe8f}s{3pFyK@cKm1piU1Kapw0o%R*f;D0AEBygkg`W+aR0A zPSyv&d6_nH`q*4x)`&Qn7P>ntIHTs_?Hz zD?Zm4aacG+ZzCdb8Ei^9nPTT@0$yfR#2_c~<%g3EtDT=;7=%&`Eatf#kdH)(eS?v$ z6#bvI@vL|xtx~Yx7zl4`C2Z~H2S409+-1R@59In3SL^G2m|yhDa~7nk=>}dVAa@fq z{3K$xjsq~u5_T}U*DFN?WKK1rN*#N#ax5VJQr7Bl{@38zO7AP7VZZVHpWCL=U z&zF?Vq66<*nmgDHKP-GD1E3vxEc~unRc#OixG`9o%#RKFh~-n(2_HYT10!VN;CWN8 z9XR+Q3weE$ERUWHt5M$33kGDz=K8Kv4KaDMx}~n`!6Ed}qUplZzO68(WX05{4LNjf z7Hn5O*;V*%A7D5S+j9eW74x%NJlqu>GD&J$%W5XBsD6?ow7NUDykzZuOq%UgzY4%G zT^wv-<|Bt7kBnfchmGKG{GAZSW-zt_DG2dEvDr`xZt zRgLDQHrbs?e7Ht3R0KjIY!lGQ6a(So>3l8kG zWR6jmWZNL)s&u6OPq~~D3)?v$c7yaYco!ncEqg`6423byTPnZ5YAtR%5+P8e9NxaV<=o0<5kN;jnzQW!AhpY3>E$FgG Q{<8o+UICsJ8=;JU17L2V3jhEB literal 0 HcmV?d00001 diff --git a/test/constraint/pt_face_distance/normal.slvs b/test/constraint/pt_face_distance/normal.slvs new file mode 100644 index 0000000..f2fd334 --- /dev/null +++ b/test/constraint/pt_face_distance/normal.slvs @@ -0,0 +1,713 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=00020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000003 +Group.type=5100 +Group.order=2 +Group.name=extrude +Group.activeWorkplane.v=00010000 +Group.opA.v=00000002 +Group.color=00646464 +Group.subtype=7000 +Group.skipFirst=0 +Group.predef.entityB.v=00020000 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ + 1 00040000 1002 + 2 00040001 1002 + 3 00040020 1002 + 4 00040040 1002 + 5 00040000 1001 + 6 00040001 1001 + 7 00040020 1001 + 8 00040040 1001 + 9 00040001 1003 + 10 80020000 1002 + 11 80020000 1001 + 12 80020001 1002 + 13 80020002 1002 + 14 80020001 1001 + 15 80020002 1001 + 16 80020002 1003 + 17 00000000 1001 + 18 00000000 1002 +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +AddParam + +Param.h.v.=00040011 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=80030000 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=80030001 +AddParam + +Param.h.v.=80030002 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=00020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=00010000 +Request.group.v=00000003 +Request.construction=1 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=00020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=00020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=00020000 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=00020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=1 +Entity.workplane.v=00010000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=0 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030001 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.normal.v=80030003 +Entity.distance.v=80030004 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030002 +Entity.type=2010 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030003 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030004 +Entity.type=4001 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030005 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.normal.v=80030007 +Entity.distance.v=80030008 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030006 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030007 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030008 +Entity.type=4001 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030009 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.point[1].v=80030002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000c +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003000d +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000d +Entity.type=2010 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000e +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000f +Entity.type=2010 +Entity.construction=1 +Entity.actPoint.x=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030010 +Entity.type=11000 +Entity.construction=1 +Entity.point[0].v=8003000f +Entity.point[1].v=8003000d +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030011 +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.actPoint.x=10.00000000000000000000 +Entity.actNormal.vx=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030012 +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003000d +Entity.actNormal.vx=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=00020000 +Constraint.ptA.v=00010001 +Constraint.ptB.v=00040001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=33 +Constraint.group.v=00000003 +Constraint.workplane.v=00010000 +Constraint.valA=-10.00000000000000000000 +Constraint.ptA.v=00050000 +Constraint.entityA.v=80030012 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Surface 00000001 00646464 80030012 1 1 +SCtrl 0 0 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 0.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 0.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000a 0 0.00000000000000000000 -0.00000000000000088818 -5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 -0.00000000000000088818 +TrimBy 00000001 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000004 0 0.00000000000000000000 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 +TrimBy 00000007 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 0.00000000000000000000 -0.00000000000000088818 -5.00000000000000000000 +AddSurface +Surface 00000002 00646464 80030011 1 1 +SCtrl 0 0 10.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 10.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 10.00000000000000000000 5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000005 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 10.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000002 1 10.00000000000000000000 0.00000000000000000000 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 0000000b 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000088818 10.00000000000000000000 -0.00000000000000044409 -5.00000000000000000000 +TrimBy 00000008 1 10.00000000000000000000 -0.00000000000000044409 -5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 +AddSurface +Surface 00000003 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 1 1 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 2 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000002 0 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +TrimBy 00000001 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 0000000c 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 00000003 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddSurface +Surface 00000004 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 -4.99999999999999911182 5.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 1 1 10.00000000000000000000 -4.99999999999999911182 5.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 2 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +TrimBy 00000005 0 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +TrimBy 00000004 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +TrimBy 00000003 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +TrimBy 00000006 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddSurface +Surface 00000005 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 -5.00000000000000088818 -4.99999999999999911182 Weight 0.70710678118654757274 +SCtrl 1 1 10.00000000000000000000 -5.00000000000000088818 -4.99999999999999911182 Weight 0.70710678118654757274 +SCtrl 2 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000008 0 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +TrimBy 00000007 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +TrimBy 00000006 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +TrimBy 00000009 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddSurface +Surface 00000006 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 4.99999999999999911182 -5.00000000000000088818 Weight 0.70710678118654757274 +SCtrl 1 1 10.00000000000000000000 4.99999999999999911182 -5.00000000000000088818 Weight 0.70710678118654757274 +SCtrl 2 0 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +TrimBy 0000000b 0 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +TrimBy 0000000a 1 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +TrimBy 00000009 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +TrimBy 0000000c 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddSurface +Curve 00000001 1 2 00000001 00000003 +CCtrl 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +CurvePt 0 0.00000000000000000000 4.91652684208287205081 0.90981526206072360630 +CurvePt 0 0.00000000000000000000 4.64894150531215188948 1.84047354780936389673 +CurvePt 0 0.00000000000000000000 4.18497755609325672310 2.73604876692571252761 +CurvePt 0 0.00000000000000000000 3.53553390593273775266 3.53553390593273775266 +CurvePt 0 0.00000000000000000000 2.73604876692571252761 4.18497755609325672310 +CurvePt 0 0.00000000000000000000 1.84047354780936411878 4.64894150531215188948 +CurvePt 0 0.00000000000000000000 0.90981526206072382834 4.91652684208287205081 +CurvePt 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddCurve +Curve 00000002 1 2 00000002 00000003 +CCtrl 0 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +CurvePt 0 10.00000000000000000000 4.91652684208287205081 0.90981526206072360630 +CurvePt 0 10.00000000000000000000 4.64894150531215188948 1.84047354780936389673 +CurvePt 0 10.00000000000000000000 4.18497755609325672310 2.73604876692571252761 +CurvePt 0 10.00000000000000000000 3.53553390593273775266 3.53553390593273775266 +CurvePt 0 10.00000000000000000000 2.73604876692571252761 4.18497755609325672310 +CurvePt 0 10.00000000000000000000 1.84047354780936411878 4.64894150531215188948 +CurvePt 0 10.00000000000000000000 0.90981526206072382834 4.91652684208287205081 +CurvePt 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddCurve +Curve 00000003 1 1 00000003 00000004 +CCtrl 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +CurvePt 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddCurve +Curve 00000004 1 2 00000001 00000004 +CCtrl 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 -4.99999999999999911182 5.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +CurvePt 0 0.00000000000000000000 -0.90981526206072327323 4.91652684208287205081 +CurvePt 0 0.00000000000000000000 -1.84047354780936323060 4.64894150531215188948 +CurvePt 0 0.00000000000000000000 -2.73604876692571208352 4.18497755609325672310 +CurvePt 0 0.00000000000000000000 -3.53553390593273775266 3.53553390593273775266 +CurvePt 0 0.00000000000000000000 -4.18497755609325672310 2.73604876692571297170 +CurvePt 0 0.00000000000000000000 -4.64894150531215188948 1.84047354780936434082 +CurvePt 0 0.00000000000000000000 -4.91652684208287205081 0.90981526206072405039 +CurvePt 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddCurve +Curve 00000005 1 2 00000002 00000004 +CCtrl 0 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -4.99999999999999911182 5.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +CurvePt 0 10.00000000000000000000 -0.90981526206072327323 4.91652684208287205081 +CurvePt 0 10.00000000000000000000 -1.84047354780936323060 4.64894150531215188948 +CurvePt 0 10.00000000000000000000 -2.73604876692571208352 4.18497755609325672310 +CurvePt 0 10.00000000000000000000 -3.53553390593273775266 3.53553390593273775266 +CurvePt 0 10.00000000000000000000 -4.18497755609325672310 2.73604876692571297170 +CurvePt 0 10.00000000000000000000 -4.64894150531215188948 1.84047354780936434082 +CurvePt 0 10.00000000000000000000 -4.91652684208287205081 0.90981526206072405039 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddCurve +Curve 00000006 1 1 00000004 00000005 +CCtrl 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddCurve +Curve 00000007 1 2 00000001 00000005 +CCtrl 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 -5.00000000000000088818 -4.99999999999999911182 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +CurvePt 0 0.00000000000000000000 -4.91652684208287205081 -0.90981526206072305119 +CurvePt 0 0.00000000000000000000 -4.64894150531215188948 -1.84047354780936300855 +CurvePt 0 0.00000000000000000000 -4.18497755609325672310 -2.73604876692571208352 +CurvePt 0 0.00000000000000000000 -3.53553390593273864084 -3.53553390593273730858 +CurvePt 0 0.00000000000000000000 -2.73604876692571297170 -4.18497755609325672310 +CurvePt 0 0.00000000000000000000 -1.84047354780936478491 -4.64894150531215188948 +CurvePt 0 0.00000000000000000000 -0.90981526206072438345 -4.91652684208287205081 +CurvePt 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddCurve +Curve 00000008 1 2 00000002 00000005 +CCtrl 0 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -5.00000000000000088818 -4.99999999999999911182 Weight 0.70710678118654757274 +CCtrl 2 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +CurvePt 0 10.00000000000000000000 -4.91652684208287205081 -0.90981526206072305119 +CurvePt 0 10.00000000000000000000 -4.64894150531215188948 -1.84047354780936300855 +CurvePt 0 10.00000000000000000000 -4.18497755609325672310 -2.73604876692571208352 +CurvePt 0 10.00000000000000000000 -3.53553390593273864084 -3.53553390593273730858 +CurvePt 0 10.00000000000000000000 -2.73604876692571297170 -4.18497755609325672310 +CurvePt 0 10.00000000000000000000 -1.84047354780936478491 -4.64894150531215188948 +CurvePt 0 10.00000000000000000000 -0.90981526206072438345 -4.91652684208287205081 +CurvePt 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddCurve +Curve 00000009 1 1 00000005 00000006 +CCtrl 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +CurvePt 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddCurve +Curve 0000000a 1 2 00000001 00000006 +CCtrl 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 4.99999999999999911182 -5.00000000000000088818 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +CurvePt 0 0.00000000000000000000 0.90981526206072282914 -4.91652684208287205081 +CurvePt 0 0.00000000000000000000 1.84047354780936300855 -4.64894150531215188948 +CurvePt 0 0.00000000000000000000 2.73604876692571163943 -4.18497755609325672310 +CurvePt 0 0.00000000000000000000 3.53553390593273730858 -3.53553390593273864084 +CurvePt 0 0.00000000000000000000 4.18497755609325583492 -2.73604876692571297170 +CurvePt 0 0.00000000000000000000 4.64894150531215188948 -1.84047354780936500696 +CurvePt 0 0.00000000000000000000 4.91652684208287205081 -0.90981526206072471652 +CurvePt 1 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddCurve +Curve 0000000b 1 2 00000002 00000006 +CCtrl 0 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 4.99999999999999911182 -5.00000000000000088818 Weight 0.70710678118654757274 +CCtrl 2 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +CurvePt 0 10.00000000000000000000 0.90981526206072282914 -4.91652684208287205081 +CurvePt 0 10.00000000000000000000 1.84047354780936300855 -4.64894150531215188948 +CurvePt 0 10.00000000000000000000 2.73604876692571163943 -4.18497755609325672310 +CurvePt 0 10.00000000000000000000 3.53553390593273730858 -3.53553390593273864084 +CurvePt 0 10.00000000000000000000 4.18497755609325583492 -2.73604876692571297170 +CurvePt 0 10.00000000000000000000 4.64894150531215188948 -1.84047354780936500696 +CurvePt 0 10.00000000000000000000 4.91652684208287205081 -0.90981526206072471652 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddCurve +Curve 0000000c 1 1 00000006 00000003 +CCtrl 0 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddCurve diff --git a/test/constraint/pt_face_distance/normal_v20.slvs b/test/constraint/pt_face_distance/normal_v20.slvs new file mode 100644 index 0000000..c8a4c23 --- /dev/null +++ b/test/constraint/pt_face_distance/normal_v20.slvs @@ -0,0 +1,712 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=00020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000003 +Group.type=5100 +Group.order=2 +Group.name=extrude +Group.activeWorkplane.v=00010000 +Group.opA.v=00000002 +Group.color=00646464 +Group.subtype=7000 +Group.skipFirst=0 +Group.predef.entityB.v=00020000 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ + 1 00040000 1002 + 2 00040001 1002 + 3 00040020 1002 + 4 00040040 1002 + 5 00040000 1001 + 6 00040001 1001 + 7 00040020 1001 + 8 00040040 1001 + 9 00040001 1003 + 10 80020000 1002 + 11 80020000 1001 + 12 80020001 1002 + 13 80020002 1002 + 14 80020001 1001 + 15 80020002 1001 + 16 80020002 1003 + 17 00000000 1001 + 18 00000000 1002 +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +AddParam + +Param.h.v.=00040011 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=80030000 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=80030001 +AddParam + +Param.h.v.=80030002 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=00020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=00010000 +Request.group.v=00000003 +Request.construction=1 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=00020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=00020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=00020000 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=00020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=00010000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=0 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030001 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.normal.v=80030003 +Entity.distance.v=80030004 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030002 +Entity.type=2010 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030003 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030004 +Entity.type=4001 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030005 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.normal.v=80030007 +Entity.distance.v=80030008 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030006 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030007 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030008 +Entity.type=4001 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030009 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.point[1].v=80030002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000c +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003000d +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000d +Entity.type=2010 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000e +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000f +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030010 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.point[1].v=8003000d +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030011 +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.actPoint.x=10.00000000000000000000 +Entity.actNormal.vx=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030012 +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003000d +Entity.actNormal.vx=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=00020000 +Constraint.ptA.v=00010001 +Constraint.ptB.v=00040001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=33 +Constraint.group.v=00000003 +Constraint.workplane.v=00010000 +Constraint.valA=-10.00000000000000000000 +Constraint.ptA.v=00050000 +Constraint.entityA.v=80030012 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Surface 00000001 00646464 80030012 1 1 +SCtrl 0 0 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 0.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 0.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000a 0 0.00000000000000000000 -0.00000000000000088818 -5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 -0.00000000000000088818 +TrimBy 00000001 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000004 0 0.00000000000000000000 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 +TrimBy 00000007 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 0.00000000000000000000 -0.00000000000000088818 -5.00000000000000000000 +AddSurface +Surface 00000002 00646464 80030011 1 1 +SCtrl 0 0 10.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 10.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 10.00000000000000000000 5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000005 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 10.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000002 1 10.00000000000000000000 0.00000000000000000000 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 0000000b 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000088818 10.00000000000000000000 -0.00000000000000044409 -5.00000000000000000000 +TrimBy 00000008 1 10.00000000000000000000 -0.00000000000000044409 -5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 +AddSurface +Surface 00000003 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757000 +SCtrl 1 1 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757000 +SCtrl 2 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000002 0 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +TrimBy 00000003 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +TrimBy 00000001 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 0000000c 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +AddSurface +Surface 00000004 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 -4.99999999999999910000 5.00000000000000000000 Weight 0.70710678118654757000 +SCtrl 1 1 10.00000000000000000000 -4.99999999999999910000 5.00000000000000000000 Weight 0.70710678118654757000 +SCtrl 2 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +TrimBy 00000005 0 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +TrimBy 00000006 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +TrimBy 00000004 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +TrimBy 00000003 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddSurface +Surface 00000005 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 -5.00000000000000090000 -4.99999999999999910000 Weight 0.70710678118654757000 +SCtrl 1 1 10.00000000000000000000 -5.00000000000000090000 -4.99999999999999910000 Weight 0.70710678118654757000 +SCtrl 2 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000008 0 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +TrimBy 00000009 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +TrimBy 00000007 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +TrimBy 00000006 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddSurface +Surface 00000006 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 4.99999999999999910000 -5.00000000000000090000 Weight 0.70710678118654757000 +SCtrl 1 1 10.00000000000000000000 4.99999999999999910000 -5.00000000000000090000 Weight 0.70710678118654757000 +SCtrl 2 0 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +TrimBy 0000000b 0 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +TrimBy 0000000c 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +TrimBy 0000000a 1 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +TrimBy 00000009 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddSurface +Curve 00000001 1 2 00000001 00000003 +CCtrl 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757000 +CCtrl 2 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +CurvePt 0 0.00000000000000000000 4.91652684208287210000 0.90981526206072361000 +CurvePt 0 0.00000000000000000000 4.64894150531215190000 1.84047354780936390000 +CurvePt 0 0.00000000000000000000 4.18497755609325670000 2.73604876692571250000 +CurvePt 0 0.00000000000000000000 3.53553390593273780000 3.53553390593273780000 +CurvePt 0 0.00000000000000000000 2.73604876692571250000 4.18497755609325670000 +CurvePt 0 0.00000000000000000000 1.84047354780936410000 4.64894150531215190000 +CurvePt 0 0.00000000000000000000 0.90981526206072383000 4.91652684208287210000 +CurvePt 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddCurve +Curve 00000002 1 2 00000002 00000003 +CCtrl 0 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757000 +CCtrl 2 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +CurvePt 0 10.00000000000000000000 4.91652684208287210000 0.90981526206072361000 +CurvePt 0 10.00000000000000000000 4.64894150531215190000 1.84047354780936390000 +CurvePt 0 10.00000000000000000000 4.18497755609325670000 2.73604876692571250000 +CurvePt 0 10.00000000000000000000 3.53553390593273780000 3.53553390593273780000 +CurvePt 0 10.00000000000000000000 2.73604876692571250000 4.18497755609325670000 +CurvePt 0 10.00000000000000000000 1.84047354780936410000 4.64894150531215190000 +CurvePt 0 10.00000000000000000000 0.90981526206072383000 4.91652684208287210000 +CurvePt 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddCurve +Curve 00000003 1 1 00000003 00000004 +CCtrl 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +CurvePt 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddCurve +Curve 00000004 1 2 00000001 00000004 +CCtrl 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 -4.99999999999999910000 5.00000000000000000000 Weight 0.70710678118654757000 +CCtrl 2 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +CurvePt 0 0.00000000000000000000 -0.90981526206072327000 4.91652684208287210000 +CurvePt 0 0.00000000000000000000 -1.84047354780936320000 4.64894150531215190000 +CurvePt 0 0.00000000000000000000 -2.73604876692571210000 4.18497755609325670000 +CurvePt 0 0.00000000000000000000 -3.53553390593273780000 3.53553390593273780000 +CurvePt 0 0.00000000000000000000 -4.18497755609325670000 2.73604876692571300000 +CurvePt 0 0.00000000000000000000 -4.64894150531215190000 1.84047354780936430000 +CurvePt 0 0.00000000000000000000 -4.91652684208287210000 0.90981526206072405000 +CurvePt 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddCurve +Curve 00000005 1 2 00000002 00000004 +CCtrl 0 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -4.99999999999999910000 5.00000000000000000000 Weight 0.70710678118654757000 +CCtrl 2 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +CurvePt 0 10.00000000000000000000 -0.90981526206072327000 4.91652684208287210000 +CurvePt 0 10.00000000000000000000 -1.84047354780936320000 4.64894150531215190000 +CurvePt 0 10.00000000000000000000 -2.73604876692571210000 4.18497755609325670000 +CurvePt 0 10.00000000000000000000 -3.53553390593273780000 3.53553390593273780000 +CurvePt 0 10.00000000000000000000 -4.18497755609325670000 2.73604876692571300000 +CurvePt 0 10.00000000000000000000 -4.64894150531215190000 1.84047354780936430000 +CurvePt 0 10.00000000000000000000 -4.91652684208287210000 0.90981526206072405000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddCurve +Curve 00000006 1 1 00000004 00000005 +CCtrl 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddCurve +Curve 00000007 1 2 00000001 00000005 +CCtrl 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 -5.00000000000000090000 -4.99999999999999910000 Weight 0.70710678118654757000 +CCtrl 2 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +CurvePt 0 0.00000000000000000000 -4.91652684208287210000 -0.90981526206072305000 +CurvePt 0 0.00000000000000000000 -4.64894150531215190000 -1.84047354780936300000 +CurvePt 0 0.00000000000000000000 -4.18497755609325670000 -2.73604876692571210000 +CurvePt 0 0.00000000000000000000 -3.53553390593273860000 -3.53553390593273730000 +CurvePt 0 0.00000000000000000000 -2.73604876692571300000 -4.18497755609325670000 +CurvePt 0 0.00000000000000000000 -1.84047354780936480000 -4.64894150531215190000 +CurvePt 0 0.00000000000000000000 -0.90981526206072438000 -4.91652684208287210000 +CurvePt 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddCurve +Curve 00000008 1 2 00000002 00000005 +CCtrl 0 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -5.00000000000000090000 -4.99999999999999910000 Weight 0.70710678118654757000 +CCtrl 2 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +CurvePt 0 10.00000000000000000000 -4.91652684208287210000 -0.90981526206072305000 +CurvePt 0 10.00000000000000000000 -4.64894150531215190000 -1.84047354780936300000 +CurvePt 0 10.00000000000000000000 -4.18497755609325670000 -2.73604876692571210000 +CurvePt 0 10.00000000000000000000 -3.53553390593273860000 -3.53553390593273730000 +CurvePt 0 10.00000000000000000000 -2.73604876692571300000 -4.18497755609325670000 +CurvePt 0 10.00000000000000000000 -1.84047354780936480000 -4.64894150531215190000 +CurvePt 0 10.00000000000000000000 -0.90981526206072438000 -4.91652684208287210000 +CurvePt 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddCurve +Curve 00000009 1 1 00000005 00000006 +CCtrl 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +CurvePt 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddCurve +Curve 0000000a 1 2 00000001 00000006 +CCtrl 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 4.99999999999999910000 -5.00000000000000090000 Weight 0.70710678118654757000 +CCtrl 2 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +CurvePt 0 0.00000000000000000000 0.90981526206072283000 -4.91652684208287210000 +CurvePt 0 0.00000000000000000000 1.84047354780936300000 -4.64894150531215190000 +CurvePt 0 0.00000000000000000000 2.73604876692571160000 -4.18497755609325670000 +CurvePt 0 0.00000000000000000000 3.53553390593273730000 -3.53553390593273860000 +CurvePt 0 0.00000000000000000000 4.18497755609325580000 -2.73604876692571300000 +CurvePt 0 0.00000000000000000000 4.64894150531215190000 -1.84047354780936500000 +CurvePt 0 0.00000000000000000000 4.91652684208287210000 -0.90981526206072472000 +CurvePt 1 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddCurve +Curve 0000000b 1 2 00000002 00000006 +CCtrl 0 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 4.99999999999999910000 -5.00000000000000090000 Weight 0.70710678118654757000 +CCtrl 2 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +CurvePt 0 10.00000000000000000000 0.90981526206072283000 -4.91652684208287210000 +CurvePt 0 10.00000000000000000000 1.84047354780936300000 -4.64894150531215190000 +CurvePt 0 10.00000000000000000000 2.73604876692571160000 -4.18497755609325670000 +CurvePt 0 10.00000000000000000000 3.53553390593273730000 -3.53553390593273860000 +CurvePt 0 10.00000000000000000000 4.18497755609325580000 -2.73604876692571300000 +CurvePt 0 10.00000000000000000000 4.64894150531215190000 -1.84047354780936500000 +CurvePt 0 10.00000000000000000000 4.91652684208287210000 -0.90981526206072472000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddCurve +Curve 0000000c 1 1 00000006 00000003 +CCtrl 0 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddCurve diff --git a/test/constraint/pt_face_distance/normal_v22.slvs b/test/constraint/pt_face_distance/normal_v22.slvs new file mode 100644 index 0000000..2e4195e --- /dev/null +++ b/test/constraint/pt_face_distance/normal_v22.slvs @@ -0,0 +1,681 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=00020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000003 +Group.type=5100 +Group.order=2 +Group.name=extrude +Group.activeWorkplane.v=00010000 +Group.opA.v=00000002 +Group.color=00646464 +Group.subtype=7000 +Group.skipFirst=0 +Group.predef.entityB.v=00020000 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ + 1 00040000 1002 + 2 00040001 1002 + 3 00040020 1002 + 4 00040040 1002 + 5 00040000 1001 + 6 00040001 1001 + 7 00040020 1001 + 8 00040040 1001 + 9 00040001 1003 + 10 80020000 1002 + 11 80020000 1001 + 12 80020001 1002 + 13 80020002 1002 + 14 80020001 1001 + 15 80020002 1001 + 16 80020002 1003 + 17 00000000 1001 + 18 00000000 1002 +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +AddParam + +Param.h.v.=00040011 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=80030000 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=80030001 +AddParam + +Param.h.v.=80030002 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=00020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=00010000 +Request.group.v=00000003 +Request.construction=1 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=00020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=00020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=00020000 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=00020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=00010000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=0 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030001 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.normal.v=80030003 +Entity.distance.v=80030004 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030002 +Entity.type=2010 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030003 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030004 +Entity.type=4001 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030005 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.normal.v=80030007 +Entity.distance.v=80030008 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030006 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030007 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030008 +Entity.type=4001 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030009 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.point[1].v=80030002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000c +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003000d +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000d +Entity.type=2010 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000e +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000f +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030010 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.point[1].v=8003000d +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030011 +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.actPoint.x=10.00000000000000000000 +Entity.actNormal.vx=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030012 +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003000d +Entity.actNormal.vx=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=00020000 +Constraint.ptA.v=00010001 +Constraint.ptB.v=00040001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=33 +Constraint.group.v=00000003 +Constraint.workplane.v=00010000 +Constraint.valA=-10.00000000000000000000 +Constraint.ptA.v=00050000 +Constraint.entityA.v=80030012 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Surface 00000001 00646464 80030012 1 1 +SCtrl 0 0 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 0.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 0.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000a 0 0.00000000000000000000 -0.00000000000000088818 -5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 -0.00000000000000088818 +TrimBy 00000001 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000004 0 0.00000000000000000000 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 +TrimBy 00000007 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 0.00000000000000000000 -0.00000000000000088818 -5.00000000000000000000 +AddSurface +Surface 00000002 00646464 80030011 1 1 +SCtrl 0 0 10.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 10.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 10.00000000000000000000 5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000005 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 10.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000002 1 10.00000000000000000000 0.00000000000000000000 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 0000000b 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000088818 10.00000000000000000000 -0.00000000000000044409 -5.00000000000000000000 +TrimBy 00000008 1 10.00000000000000000000 -0.00000000000000044409 -5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 +AddSurface +Surface 00000003 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 1 1 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 2 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000002 0 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +TrimBy 00000001 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 0000000c 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 00000003 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddSurface +Surface 00000004 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 -4.99999999999999911182 5.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 1 1 10.00000000000000000000 -4.99999999999999911182 5.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 2 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +TrimBy 00000005 0 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +TrimBy 00000004 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +TrimBy 00000003 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +TrimBy 00000006 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddSurface +Surface 00000005 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 -5.00000000000000088818 -4.99999999999999911182 Weight 0.70710678118654757274 +SCtrl 1 1 10.00000000000000000000 -5.00000000000000088818 -4.99999999999999911182 Weight 0.70710678118654757274 +SCtrl 2 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000008 0 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +TrimBy 00000007 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +TrimBy 00000006 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +TrimBy 00000009 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddSurface +Surface 00000006 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 4.99999999999999911182 -5.00000000000000088818 Weight 0.70710678118654757274 +SCtrl 1 1 10.00000000000000000000 4.99999999999999911182 -5.00000000000000088818 Weight 0.70710678118654757274 +SCtrl 2 0 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +TrimBy 0000000b 0 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +TrimBy 0000000a 1 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +TrimBy 00000009 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +TrimBy 0000000c 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddSurface +Curve 00000001 1 2 00000001 00000003 +CCtrl 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +CurvePt 0 0.00000000000000000000 4.64894150531215188948 1.84047354780936389673 +CurvePt 0 0.00000000000000000000 3.53553390593273775266 3.53553390593273775266 +CurvePt 0 0.00000000000000000000 1.84047354780936411878 4.64894150531215188948 +CurvePt 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddCurve +Curve 00000002 1 2 00000002 00000003 +CCtrl 0 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +CurvePt 0 10.00000000000000000000 4.64894150531215188948 1.84047354780936389673 +CurvePt 0 10.00000000000000000000 3.53553390593273775266 3.53553390593273775266 +CurvePt 0 10.00000000000000000000 1.84047354780936411878 4.64894150531215188948 +CurvePt 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddCurve +Curve 00000003 1 1 00000003 00000004 +CCtrl 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +CurvePt 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddCurve +Curve 00000004 1 2 00000001 00000004 +CCtrl 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 -4.99999999999999911182 5.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +CurvePt 0 0.00000000000000000000 -1.84047354780936323060 4.64894150531215188948 +CurvePt 0 0.00000000000000000000 -3.53553390593273775266 3.53553390593273775266 +CurvePt 0 0.00000000000000000000 -4.64894150531215188948 1.84047354780936434082 +CurvePt 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddCurve +Curve 00000005 1 2 00000002 00000004 +CCtrl 0 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -4.99999999999999911182 5.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +CurvePt 0 10.00000000000000000000 -1.84047354780936323060 4.64894150531215188948 +CurvePt 0 10.00000000000000000000 -3.53553390593273775266 3.53553390593273775266 +CurvePt 0 10.00000000000000000000 -4.64894150531215188948 1.84047354780936434082 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddCurve +Curve 00000006 1 1 00000004 00000005 +CCtrl 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddCurve +Curve 00000007 1 2 00000001 00000005 +CCtrl 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 -5.00000000000000088818 -4.99999999999999911182 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +CurvePt 0 0.00000000000000000000 -4.64894150531215188948 -1.84047354780936300855 +CurvePt 0 0.00000000000000000000 -3.53553390593273864084 -3.53553390593273730858 +CurvePt 0 0.00000000000000000000 -1.84047354780936478491 -4.64894150531215188948 +CurvePt 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddCurve +Curve 00000008 1 2 00000002 00000005 +CCtrl 0 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -5.00000000000000088818 -4.99999999999999911182 Weight 0.70710678118654757274 +CCtrl 2 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +CurvePt 0 10.00000000000000000000 -4.64894150531215188948 -1.84047354780936300855 +CurvePt 0 10.00000000000000000000 -3.53553390593273864084 -3.53553390593273730858 +CurvePt 0 10.00000000000000000000 -1.84047354780936478491 -4.64894150531215188948 +CurvePt 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddCurve +Curve 00000009 1 1 00000005 00000006 +CCtrl 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +CurvePt 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddCurve +Curve 0000000a 1 2 00000001 00000006 +CCtrl 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 4.99999999999999911182 -5.00000000000000088818 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +CurvePt 0 0.00000000000000000000 1.84047354780936300855 -4.64894150531215188948 +CurvePt 0 0.00000000000000000000 3.53553390593273730858 -3.53553390593273864084 +CurvePt 0 0.00000000000000000000 4.64894150531215188948 -1.84047354780936500696 +CurvePt 1 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddCurve +Curve 0000000b 1 2 00000002 00000006 +CCtrl 0 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 4.99999999999999911182 -5.00000000000000088818 Weight 0.70710678118654757274 +CCtrl 2 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +CurvePt 0 10.00000000000000000000 1.84047354780936300855 -4.64894150531215188948 +CurvePt 0 10.00000000000000000000 3.53553390593273730858 -3.53553390593273864084 +CurvePt 0 10.00000000000000000000 4.64894150531215188948 -1.84047354780936500696 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddCurve +Curve 0000000c 1 1 00000006 00000003 +CCtrl 0 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddCurve diff --git a/test/constraint/pt_face_distance/reference.png b/test/constraint/pt_face_distance/reference.png new file mode 100644 index 0000000000000000000000000000000000000000..6e392b3c61c4a9e483479e07a2c677ede3fb41ff GIT binary patch literal 4378 zcmeHLc~FyQ9)9yd1cJz21O=pCWeA5>i6W;0Y9T~W4k6MYC~XngC9psuIl#hd1x682 zL}2ZT2SfrIE=7V!P*6e_2owQBQ4B{o3gHgy7u{h^%`n@Y-Th<7KQi;q_kA<(^Shqk z^W^LyxBV(8Jrn>y<=_EF4*(z^$xm?={N&Ne%UuAd^&WI|I1-aB?8ZjCbT&xp^*+fVIhN2q2cUZ<~(5};l1saYxOG|F?LXf?1@|80B(yb~FN zl)5pB{)S=+*m;Fg@cvP`=F}6na}do}uTLaORq)hz3`ScTT>Z1367sI^KPBW-hrB1d z|M z0})+vWG4Ts{M>wOfrH=m6OAEex;nHWH3^ACWh(w~_w3`{JgPpvfByK(A!k1u(#WrT zVx~Pew#kY7bj{-Uu0U0*v0>Bxn;-DN?y#_A2JM{o%KCOR_v{>^AL6gp1< zQeJaxc00etuDzZl!$Q+juM zXzYv4eg1a_y?YlH=SRKu4L3LtYSOE;_C0IQp2&9bCy!uVr%QsB^Wt63NJ2F63EPgs z^I{=V~`z$l-hD+ChcxV0t{oB#))Uu1P$iZ3m!H|TAGAZ z`?B6%g!BI4N*uJT;KKFr_)|wY*-R8=&({ z?3NaYA}!!{$FC+L%^eU9nW;k`EZvi7dz!T|dPovM8meR|y?YlOp&)rj+x7xp66Jvq zd@^mk`BcMqnjsi6XM(luB@1@<`H90dP~|VqKjL418)WJt#Fs03F==t+rLmZ?S}JvB zqDCkdE{W%-%8Gspvq?;ELx<&=F;6GR1z)yx4JFN&lJu-4r#TBxT;FM&x$a!_pG`{Q zk!*2sR$V%l!D79Bnmdn;8`PWI+?Pu{)-%JwEJktc)|35x{oD*kb6-)Zi`fPXW1@D> z()01r$Hk+)NB*k2MC#KSeo&x-UZ+AooHHUk+K;Od@f)es*!kJ63d2|8n1!_PFifiA z)t=FP?%^@;)}F3coyA{aLv!2a6)tdt+6EW3;uy%!nBl6K*Lb3qu*CMFuXWrK>l;f9 z?I@=hEK96@Ra)~MZOkS(7gs$^P~})T(5K<8b(^eX#MZ@s-j!))re3+}fDs)T=|B*= z=+Keh<3E@G*JTj9+<iaDN@KWNMjTaMh4#lt%)07bYlRz&rY zrfg1)y(>$Z6q+%IZve3B#K+AKYZ}@gO-STp^K31$Kz6y38Tb8ip=ct=ttMfU#48?nnXZoFnnXYh9S%TSEr(*45& z@cy97?xz*3)_Xr9!2T4wPpa0i`Iv%TVuI|clL>@X>V^9y-{9YYvgckY`2ouooZn9n zUTTgHmUt6dj}~mOculUtqUtT~K?r-QJkL$vIine(Q4|aH?`0uD+#SgnS-v~QdfL#3 zVvHT~)3}0~&P6?cz^0Ttb`&+9(VE`4Xyr%Vw zr1gBIY6K)W^Wot9#^9C~v8_%kPZgI1RZn^e8G)z7)_{FwO54ZJv~eO}va3GDW_o9-MxhPzXQO$(mDVD literal 0 HcmV?d00001 diff --git a/test/constraint/pt_face_distance/reference.slvs b/test/constraint/pt_face_distance/reference.slvs new file mode 100644 index 0000000..2d89471 --- /dev/null +++ b/test/constraint/pt_face_distance/reference.slvs @@ -0,0 +1,713 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=00020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000003 +Group.type=5100 +Group.order=2 +Group.name=extrude +Group.activeWorkplane.v=00010000 +Group.opA.v=00000002 +Group.color=00646464 +Group.subtype=7000 +Group.skipFirst=0 +Group.predef.entityB.v=00020000 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ + 1 00040000 1002 + 2 00040001 1002 + 3 00040020 1002 + 4 00040040 1002 + 5 00040000 1001 + 6 00040001 1001 + 7 00040020 1001 + 8 00040040 1001 + 9 00040001 1003 + 10 80020000 1002 + 11 80020000 1001 + 12 80020001 1002 + 13 80020002 1002 + 14 80020001 1001 + 15 80020002 1001 + 16 80020002 1003 + 17 00000000 1001 + 18 00000000 1002 +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +AddParam + +Param.h.v.=00040011 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=80030000 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=80030001 +AddParam + +Param.h.v.=80030002 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=00020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=00010000 +Request.group.v=00000003 +Request.construction=1 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=00020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=00020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=00020000 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=00020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=1 +Entity.workplane.v=00010000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=0 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030001 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.normal.v=80030003 +Entity.distance.v=80030004 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030002 +Entity.type=2010 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030003 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030004 +Entity.type=4001 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030005 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.normal.v=80030007 +Entity.distance.v=80030008 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030006 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030007 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030008 +Entity.type=4001 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030009 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.point[1].v=80030002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000c +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003000d +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000d +Entity.type=2010 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000e +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000f +Entity.type=2010 +Entity.construction=1 +Entity.actPoint.x=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030010 +Entity.type=11000 +Entity.construction=1 +Entity.point[0].v=8003000f +Entity.point[1].v=8003000d +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030011 +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.actPoint.x=10.00000000000000000000 +Entity.actNormal.vx=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030012 +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003000d +Entity.actNormal.vx=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=00020000 +Constraint.ptA.v=00010001 +Constraint.ptB.v=00040001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=33 +Constraint.group.v=00000003 +Constraint.workplane.v=00010000 +Constraint.valA=-10.00000000000000000000 +Constraint.ptA.v=00050000 +Constraint.entityA.v=80030012 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=1 +AddConstraint + +Surface 00000001 00646464 80030012 1 1 +SCtrl 0 0 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 0.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 0.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000a 0 0.00000000000000000000 -0.00000000000000088818 -5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 -0.00000000000000088818 +TrimBy 00000001 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000004 0 0.00000000000000000000 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 +TrimBy 00000007 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 0.00000000000000000000 -0.00000000000000088818 -5.00000000000000000000 +AddSurface +Surface 00000002 00646464 80030011 1 1 +SCtrl 0 0 10.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 10.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 10.00000000000000000000 5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000005 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 10.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000002 1 10.00000000000000000000 0.00000000000000000000 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 0000000b 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000088818 10.00000000000000000000 -0.00000000000000044409 -5.00000000000000000000 +TrimBy 00000008 1 10.00000000000000000000 -0.00000000000000044409 -5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 +AddSurface +Surface 00000003 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 1 1 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 2 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000002 0 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +TrimBy 00000001 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 0000000c 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 00000003 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddSurface +Surface 00000004 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 -4.99999999999999911182 5.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 1 1 10.00000000000000000000 -4.99999999999999911182 5.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 2 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +TrimBy 00000005 0 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +TrimBy 00000004 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +TrimBy 00000003 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +TrimBy 00000006 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddSurface +Surface 00000005 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 -5.00000000000000088818 -4.99999999999999911182 Weight 0.70710678118654757274 +SCtrl 1 1 10.00000000000000000000 -5.00000000000000088818 -4.99999999999999911182 Weight 0.70710678118654757274 +SCtrl 2 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000008 0 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +TrimBy 00000007 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +TrimBy 00000006 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +TrimBy 00000009 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddSurface +Surface 00000006 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 4.99999999999999911182 -5.00000000000000088818 Weight 0.70710678118654757274 +SCtrl 1 1 10.00000000000000000000 4.99999999999999911182 -5.00000000000000088818 Weight 0.70710678118654757274 +SCtrl 2 0 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +TrimBy 0000000b 0 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +TrimBy 0000000a 1 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +TrimBy 00000009 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +TrimBy 0000000c 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddSurface +Curve 00000001 1 2 00000001 00000003 +CCtrl 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +CurvePt 0 0.00000000000000000000 4.91652684208287205081 0.90981526206072360630 +CurvePt 0 0.00000000000000000000 4.64894150531215188948 1.84047354780936389673 +CurvePt 0 0.00000000000000000000 4.18497755609325672310 2.73604876692571252761 +CurvePt 0 0.00000000000000000000 3.53553390593273775266 3.53553390593273775266 +CurvePt 0 0.00000000000000000000 2.73604876692571252761 4.18497755609325672310 +CurvePt 0 0.00000000000000000000 1.84047354780936411878 4.64894150531215188948 +CurvePt 0 0.00000000000000000000 0.90981526206072382834 4.91652684208287205081 +CurvePt 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddCurve +Curve 00000002 1 2 00000002 00000003 +CCtrl 0 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +CurvePt 0 10.00000000000000000000 4.91652684208287205081 0.90981526206072360630 +CurvePt 0 10.00000000000000000000 4.64894150531215188948 1.84047354780936389673 +CurvePt 0 10.00000000000000000000 4.18497755609325672310 2.73604876692571252761 +CurvePt 0 10.00000000000000000000 3.53553390593273775266 3.53553390593273775266 +CurvePt 0 10.00000000000000000000 2.73604876692571252761 4.18497755609325672310 +CurvePt 0 10.00000000000000000000 1.84047354780936411878 4.64894150531215188948 +CurvePt 0 10.00000000000000000000 0.90981526206072382834 4.91652684208287205081 +CurvePt 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddCurve +Curve 00000003 1 1 00000003 00000004 +CCtrl 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +CurvePt 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddCurve +Curve 00000004 1 2 00000001 00000004 +CCtrl 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 -4.99999999999999911182 5.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +CurvePt 0 0.00000000000000000000 -0.90981526206072327323 4.91652684208287205081 +CurvePt 0 0.00000000000000000000 -1.84047354780936323060 4.64894150531215188948 +CurvePt 0 0.00000000000000000000 -2.73604876692571208352 4.18497755609325672310 +CurvePt 0 0.00000000000000000000 -3.53553390593273775266 3.53553390593273775266 +CurvePt 0 0.00000000000000000000 -4.18497755609325672310 2.73604876692571297170 +CurvePt 0 0.00000000000000000000 -4.64894150531215188948 1.84047354780936434082 +CurvePt 0 0.00000000000000000000 -4.91652684208287205081 0.90981526206072405039 +CurvePt 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddCurve +Curve 00000005 1 2 00000002 00000004 +CCtrl 0 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -4.99999999999999911182 5.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +CurvePt 0 10.00000000000000000000 -0.90981526206072327323 4.91652684208287205081 +CurvePt 0 10.00000000000000000000 -1.84047354780936323060 4.64894150531215188948 +CurvePt 0 10.00000000000000000000 -2.73604876692571208352 4.18497755609325672310 +CurvePt 0 10.00000000000000000000 -3.53553390593273775266 3.53553390593273775266 +CurvePt 0 10.00000000000000000000 -4.18497755609325672310 2.73604876692571297170 +CurvePt 0 10.00000000000000000000 -4.64894150531215188948 1.84047354780936434082 +CurvePt 0 10.00000000000000000000 -4.91652684208287205081 0.90981526206072405039 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddCurve +Curve 00000006 1 1 00000004 00000005 +CCtrl 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddCurve +Curve 00000007 1 2 00000001 00000005 +CCtrl 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 -5.00000000000000088818 -4.99999999999999911182 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +CurvePt 0 0.00000000000000000000 -4.91652684208287205081 -0.90981526206072305119 +CurvePt 0 0.00000000000000000000 -4.64894150531215188948 -1.84047354780936300855 +CurvePt 0 0.00000000000000000000 -4.18497755609325672310 -2.73604876692571208352 +CurvePt 0 0.00000000000000000000 -3.53553390593273864084 -3.53553390593273730858 +CurvePt 0 0.00000000000000000000 -2.73604876692571297170 -4.18497755609325672310 +CurvePt 0 0.00000000000000000000 -1.84047354780936478491 -4.64894150531215188948 +CurvePt 0 0.00000000000000000000 -0.90981526206072438345 -4.91652684208287205081 +CurvePt 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddCurve +Curve 00000008 1 2 00000002 00000005 +CCtrl 0 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -5.00000000000000088818 -4.99999999999999911182 Weight 0.70710678118654757274 +CCtrl 2 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +CurvePt 0 10.00000000000000000000 -4.91652684208287205081 -0.90981526206072305119 +CurvePt 0 10.00000000000000000000 -4.64894150531215188948 -1.84047354780936300855 +CurvePt 0 10.00000000000000000000 -4.18497755609325672310 -2.73604876692571208352 +CurvePt 0 10.00000000000000000000 -3.53553390593273864084 -3.53553390593273730858 +CurvePt 0 10.00000000000000000000 -2.73604876692571297170 -4.18497755609325672310 +CurvePt 0 10.00000000000000000000 -1.84047354780936478491 -4.64894150531215188948 +CurvePt 0 10.00000000000000000000 -0.90981526206072438345 -4.91652684208287205081 +CurvePt 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddCurve +Curve 00000009 1 1 00000005 00000006 +CCtrl 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +CurvePt 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddCurve +Curve 0000000a 1 2 00000001 00000006 +CCtrl 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 4.99999999999999911182 -5.00000000000000088818 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +CurvePt 0 0.00000000000000000000 0.90981526206072282914 -4.91652684208287205081 +CurvePt 0 0.00000000000000000000 1.84047354780936300855 -4.64894150531215188948 +CurvePt 0 0.00000000000000000000 2.73604876692571163943 -4.18497755609325672310 +CurvePt 0 0.00000000000000000000 3.53553390593273730858 -3.53553390593273864084 +CurvePt 0 0.00000000000000000000 4.18497755609325583492 -2.73604876692571297170 +CurvePt 0 0.00000000000000000000 4.64894150531215188948 -1.84047354780936500696 +CurvePt 0 0.00000000000000000000 4.91652684208287205081 -0.90981526206072471652 +CurvePt 1 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddCurve +Curve 0000000b 1 2 00000002 00000006 +CCtrl 0 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 4.99999999999999911182 -5.00000000000000088818 Weight 0.70710678118654757274 +CCtrl 2 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +CurvePt 0 10.00000000000000000000 0.90981526206072282914 -4.91652684208287205081 +CurvePt 0 10.00000000000000000000 1.84047354780936300855 -4.64894150531215188948 +CurvePt 0 10.00000000000000000000 2.73604876692571163943 -4.18497755609325672310 +CurvePt 0 10.00000000000000000000 3.53553390593273730858 -3.53553390593273864084 +CurvePt 0 10.00000000000000000000 4.18497755609325583492 -2.73604876692571297170 +CurvePt 0 10.00000000000000000000 4.64894150531215188948 -1.84047354780936500696 +CurvePt 0 10.00000000000000000000 4.91652684208287205081 -0.90981526206072471652 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddCurve +Curve 0000000c 1 1 00000006 00000003 +CCtrl 0 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddCurve diff --git a/test/constraint/pt_face_distance/reference_v20.slvs b/test/constraint/pt_face_distance/reference_v20.slvs new file mode 100644 index 0000000..c506e9b --- /dev/null +++ b/test/constraint/pt_face_distance/reference_v20.slvs @@ -0,0 +1,712 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=00020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000003 +Group.type=5100 +Group.order=2 +Group.name=extrude +Group.activeWorkplane.v=00010000 +Group.opA.v=00000002 +Group.color=00646464 +Group.subtype=7000 +Group.skipFirst=0 +Group.predef.entityB.v=00020000 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ + 1 00040000 1002 + 2 00040001 1002 + 3 00040020 1002 + 4 00040040 1002 + 5 00040000 1001 + 6 00040001 1001 + 7 00040020 1001 + 8 00040040 1001 + 9 00040001 1003 + 10 80020000 1002 + 11 80020000 1001 + 12 80020001 1002 + 13 80020002 1002 + 14 80020001 1001 + 15 80020002 1001 + 16 80020002 1003 + 17 00000000 1001 + 18 00000000 1002 +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +AddParam + +Param.h.v.=00040011 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=80030000 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=80030001 +AddParam + +Param.h.v.=80030002 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=00020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=00010000 +Request.group.v=00000003 +Request.construction=1 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=00020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=00020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=00020000 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=00020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=00010000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=0 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030001 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.normal.v=80030003 +Entity.distance.v=80030004 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030002 +Entity.type=2010 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030003 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030004 +Entity.type=4001 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030005 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.normal.v=80030007 +Entity.distance.v=80030008 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030006 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030007 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030008 +Entity.type=4001 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030009 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.point[1].v=80030002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000c +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003000d +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000d +Entity.type=2010 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000e +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000f +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030010 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.point[1].v=8003000d +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030011 +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.actPoint.x=10.00000000000000000000 +Entity.actNormal.vx=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030012 +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003000d +Entity.actNormal.vx=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=00020000 +Constraint.ptA.v=00010001 +Constraint.ptB.v=00040001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=33 +Constraint.group.v=00000003 +Constraint.workplane.v=00010000 +Constraint.valA=-10.00000000000000000000 +Constraint.ptA.v=00050000 +Constraint.entityA.v=80030012 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=1 +AddConstraint + +Surface 00000001 00646464 80030012 1 1 +SCtrl 0 0 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 0.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 0.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000a 0 0.00000000000000000000 -0.00000000000000088818 -5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 -0.00000000000000088818 +TrimBy 00000001 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000004 0 0.00000000000000000000 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 +TrimBy 00000007 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 0.00000000000000000000 -0.00000000000000088818 -5.00000000000000000000 +AddSurface +Surface 00000002 00646464 80030011 1 1 +SCtrl 0 0 10.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 10.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 10.00000000000000000000 5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000005 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 10.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000002 1 10.00000000000000000000 0.00000000000000000000 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 0000000b 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000088818 10.00000000000000000000 -0.00000000000000044409 -5.00000000000000000000 +TrimBy 00000008 1 10.00000000000000000000 -0.00000000000000044409 -5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 +AddSurface +Surface 00000003 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757000 +SCtrl 1 1 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757000 +SCtrl 2 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000002 0 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +TrimBy 00000003 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +TrimBy 00000001 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 0000000c 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +AddSurface +Surface 00000004 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 -4.99999999999999910000 5.00000000000000000000 Weight 0.70710678118654757000 +SCtrl 1 1 10.00000000000000000000 -4.99999999999999910000 5.00000000000000000000 Weight 0.70710678118654757000 +SCtrl 2 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +TrimBy 00000005 0 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +TrimBy 00000006 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +TrimBy 00000004 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +TrimBy 00000003 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddSurface +Surface 00000005 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 -5.00000000000000090000 -4.99999999999999910000 Weight 0.70710678118654757000 +SCtrl 1 1 10.00000000000000000000 -5.00000000000000090000 -4.99999999999999910000 Weight 0.70710678118654757000 +SCtrl 2 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000008 0 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +TrimBy 00000009 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +TrimBy 00000007 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +TrimBy 00000006 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddSurface +Surface 00000006 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 4.99999999999999910000 -5.00000000000000090000 Weight 0.70710678118654757000 +SCtrl 1 1 10.00000000000000000000 4.99999999999999910000 -5.00000000000000090000 Weight 0.70710678118654757000 +SCtrl 2 0 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +TrimBy 0000000b 0 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +TrimBy 0000000c 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +TrimBy 0000000a 1 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +TrimBy 00000009 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddSurface +Curve 00000001 1 2 00000001 00000003 +CCtrl 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757000 +CCtrl 2 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +CurvePt 0 0.00000000000000000000 4.91652684208287210000 0.90981526206072361000 +CurvePt 0 0.00000000000000000000 4.64894150531215190000 1.84047354780936390000 +CurvePt 0 0.00000000000000000000 4.18497755609325670000 2.73604876692571250000 +CurvePt 0 0.00000000000000000000 3.53553390593273780000 3.53553390593273780000 +CurvePt 0 0.00000000000000000000 2.73604876692571250000 4.18497755609325670000 +CurvePt 0 0.00000000000000000000 1.84047354780936410000 4.64894150531215190000 +CurvePt 0 0.00000000000000000000 0.90981526206072383000 4.91652684208287210000 +CurvePt 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddCurve +Curve 00000002 1 2 00000002 00000003 +CCtrl 0 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757000 +CCtrl 2 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +CurvePt 0 10.00000000000000000000 4.91652684208287210000 0.90981526206072361000 +CurvePt 0 10.00000000000000000000 4.64894150531215190000 1.84047354780936390000 +CurvePt 0 10.00000000000000000000 4.18497755609325670000 2.73604876692571250000 +CurvePt 0 10.00000000000000000000 3.53553390593273780000 3.53553390593273780000 +CurvePt 0 10.00000000000000000000 2.73604876692571250000 4.18497755609325670000 +CurvePt 0 10.00000000000000000000 1.84047354780936410000 4.64894150531215190000 +CurvePt 0 10.00000000000000000000 0.90981526206072383000 4.91652684208287210000 +CurvePt 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddCurve +Curve 00000003 1 1 00000003 00000004 +CCtrl 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +CurvePt 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddCurve +Curve 00000004 1 2 00000001 00000004 +CCtrl 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 -4.99999999999999910000 5.00000000000000000000 Weight 0.70710678118654757000 +CCtrl 2 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +CurvePt 0 0.00000000000000000000 -0.90981526206072327000 4.91652684208287210000 +CurvePt 0 0.00000000000000000000 -1.84047354780936320000 4.64894150531215190000 +CurvePt 0 0.00000000000000000000 -2.73604876692571210000 4.18497755609325670000 +CurvePt 0 0.00000000000000000000 -3.53553390593273780000 3.53553390593273780000 +CurvePt 0 0.00000000000000000000 -4.18497755609325670000 2.73604876692571300000 +CurvePt 0 0.00000000000000000000 -4.64894150531215190000 1.84047354780936430000 +CurvePt 0 0.00000000000000000000 -4.91652684208287210000 0.90981526206072405000 +CurvePt 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddCurve +Curve 00000005 1 2 00000002 00000004 +CCtrl 0 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -4.99999999999999910000 5.00000000000000000000 Weight 0.70710678118654757000 +CCtrl 2 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +CurvePt 0 10.00000000000000000000 -0.90981526206072327000 4.91652684208287210000 +CurvePt 0 10.00000000000000000000 -1.84047354780936320000 4.64894150531215190000 +CurvePt 0 10.00000000000000000000 -2.73604876692571210000 4.18497755609325670000 +CurvePt 0 10.00000000000000000000 -3.53553390593273780000 3.53553390593273780000 +CurvePt 0 10.00000000000000000000 -4.18497755609325670000 2.73604876692571300000 +CurvePt 0 10.00000000000000000000 -4.64894150531215190000 1.84047354780936430000 +CurvePt 0 10.00000000000000000000 -4.91652684208287210000 0.90981526206072405000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddCurve +Curve 00000006 1 1 00000004 00000005 +CCtrl 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddCurve +Curve 00000007 1 2 00000001 00000005 +CCtrl 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 -5.00000000000000090000 -4.99999999999999910000 Weight 0.70710678118654757000 +CCtrl 2 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +CurvePt 0 0.00000000000000000000 -4.91652684208287210000 -0.90981526206072305000 +CurvePt 0 0.00000000000000000000 -4.64894150531215190000 -1.84047354780936300000 +CurvePt 0 0.00000000000000000000 -4.18497755609325670000 -2.73604876692571210000 +CurvePt 0 0.00000000000000000000 -3.53553390593273860000 -3.53553390593273730000 +CurvePt 0 0.00000000000000000000 -2.73604876692571300000 -4.18497755609325670000 +CurvePt 0 0.00000000000000000000 -1.84047354780936480000 -4.64894150531215190000 +CurvePt 0 0.00000000000000000000 -0.90981526206072438000 -4.91652684208287210000 +CurvePt 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddCurve +Curve 00000008 1 2 00000002 00000005 +CCtrl 0 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -5.00000000000000090000 -4.99999999999999910000 Weight 0.70710678118654757000 +CCtrl 2 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +CurvePt 0 10.00000000000000000000 -4.91652684208287210000 -0.90981526206072305000 +CurvePt 0 10.00000000000000000000 -4.64894150531215190000 -1.84047354780936300000 +CurvePt 0 10.00000000000000000000 -4.18497755609325670000 -2.73604876692571210000 +CurvePt 0 10.00000000000000000000 -3.53553390593273860000 -3.53553390593273730000 +CurvePt 0 10.00000000000000000000 -2.73604876692571300000 -4.18497755609325670000 +CurvePt 0 10.00000000000000000000 -1.84047354780936480000 -4.64894150531215190000 +CurvePt 0 10.00000000000000000000 -0.90981526206072438000 -4.91652684208287210000 +CurvePt 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddCurve +Curve 00000009 1 1 00000005 00000006 +CCtrl 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +CurvePt 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddCurve +Curve 0000000a 1 2 00000001 00000006 +CCtrl 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 4.99999999999999910000 -5.00000000000000090000 Weight 0.70710678118654757000 +CCtrl 2 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +CurvePt 0 0.00000000000000000000 0.90981526206072283000 -4.91652684208287210000 +CurvePt 0 0.00000000000000000000 1.84047354780936300000 -4.64894150531215190000 +CurvePt 0 0.00000000000000000000 2.73604876692571160000 -4.18497755609325670000 +CurvePt 0 0.00000000000000000000 3.53553390593273730000 -3.53553390593273860000 +CurvePt 0 0.00000000000000000000 4.18497755609325580000 -2.73604876692571300000 +CurvePt 0 0.00000000000000000000 4.64894150531215190000 -1.84047354780936500000 +CurvePt 0 0.00000000000000000000 4.91652684208287210000 -0.90981526206072472000 +CurvePt 1 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddCurve +Curve 0000000b 1 2 00000002 00000006 +CCtrl 0 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 4.99999999999999910000 -5.00000000000000090000 Weight 0.70710678118654757000 +CCtrl 2 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +CurvePt 0 10.00000000000000000000 0.90981526206072283000 -4.91652684208287210000 +CurvePt 0 10.00000000000000000000 1.84047354780936300000 -4.64894150531215190000 +CurvePt 0 10.00000000000000000000 2.73604876692571160000 -4.18497755609325670000 +CurvePt 0 10.00000000000000000000 3.53553390593273730000 -3.53553390593273860000 +CurvePt 0 10.00000000000000000000 4.18497755609325580000 -2.73604876692571300000 +CurvePt 0 10.00000000000000000000 4.64894150531215190000 -1.84047354780936500000 +CurvePt 0 10.00000000000000000000 4.91652684208287210000 -0.90981526206072472000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddCurve +Curve 0000000c 1 1 00000006 00000003 +CCtrl 0 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddCurve diff --git a/test/constraint/pt_face_distance/reference_v22.slvs b/test/constraint/pt_face_distance/reference_v22.slvs new file mode 100644 index 0000000..a57b808 --- /dev/null +++ b/test/constraint/pt_face_distance/reference_v22.slvs @@ -0,0 +1,681 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=00020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000003 +Group.type=5100 +Group.order=2 +Group.name=extrude +Group.activeWorkplane.v=00010000 +Group.opA.v=00000002 +Group.color=00646464 +Group.subtype=7000 +Group.skipFirst=0 +Group.predef.entityB.v=00020000 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ + 1 00040000 1002 + 2 00040001 1002 + 3 00040020 1002 + 4 00040040 1002 + 5 00040000 1001 + 6 00040001 1001 + 7 00040020 1001 + 8 00040040 1001 + 9 00040001 1003 + 10 80020000 1002 + 11 80020000 1001 + 12 80020001 1002 + 13 80020002 1002 + 14 80020001 1001 + 15 80020002 1001 + 16 80020002 1003 + 17 00000000 1001 + 18 00000000 1002 +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +AddParam + +Param.h.v.=00040011 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=80030000 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=80030001 +AddParam + +Param.h.v.=80030002 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=00020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=00010000 +Request.group.v=00000003 +Request.construction=1 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=00020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=00020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=00020000 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=00020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=00010000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=0 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030001 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.normal.v=80030003 +Entity.distance.v=80030004 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030002 +Entity.type=2010 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030003 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030004 +Entity.type=4001 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030005 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.normal.v=80030007 +Entity.distance.v=80030008 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030006 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030007 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030008 +Entity.type=4001 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030009 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.point[1].v=80030002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000c +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003000d +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000d +Entity.type=2010 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000e +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000f +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030010 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.point[1].v=8003000d +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030011 +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.actPoint.x=10.00000000000000000000 +Entity.actNormal.vx=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030012 +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003000d +Entity.actNormal.vx=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=00020000 +Constraint.ptA.v=00010001 +Constraint.ptB.v=00040001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=33 +Constraint.group.v=00000003 +Constraint.workplane.v=00010000 +Constraint.valA=-10.00000000000000000000 +Constraint.ptA.v=00050000 +Constraint.entityA.v=80030012 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=1 +AddConstraint + +Surface 00000001 00646464 80030012 1 1 +SCtrl 0 0 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 0.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 0.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000a 0 0.00000000000000000000 -0.00000000000000088818 -5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 -0.00000000000000088818 +TrimBy 00000001 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000004 0 0.00000000000000000000 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 +TrimBy 00000007 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 0.00000000000000000000 -0.00000000000000088818 -5.00000000000000000000 +AddSurface +Surface 00000002 00646464 80030011 1 1 +SCtrl 0 0 10.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 10.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 10.00000000000000000000 5.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000005 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 10.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000002 1 10.00000000000000000000 0.00000000000000000000 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 0000000b 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000088818 10.00000000000000000000 -0.00000000000000044409 -5.00000000000000000000 +TrimBy 00000008 1 10.00000000000000000000 -0.00000000000000044409 -5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 0.00000000000000044409 +AddSurface +Surface 00000003 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 1 1 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 2 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000002 0 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +TrimBy 00000001 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 0000000c 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 00000003 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddSurface +Surface 00000004 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 -4.99999999999999911182 5.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 1 1 10.00000000000000000000 -4.99999999999999911182 5.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 2 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +TrimBy 00000005 0 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +TrimBy 00000004 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +TrimBy 00000003 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +TrimBy 00000006 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddSurface +Surface 00000005 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 -5.00000000000000088818 -4.99999999999999911182 Weight 0.70710678118654757274 +SCtrl 1 1 10.00000000000000000000 -5.00000000000000088818 -4.99999999999999911182 Weight 0.70710678118654757274 +SCtrl 2 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000008 0 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +TrimBy 00000007 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +TrimBy 00000006 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +TrimBy 00000009 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddSurface +Surface 00000006 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 0.00000000000000000000 4.99999999999999911182 -5.00000000000000088818 Weight 0.70710678118654757274 +SCtrl 1 1 10.00000000000000000000 4.99999999999999911182 -5.00000000000000088818 Weight 0.70710678118654757274 +SCtrl 2 0 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +SCtrl 2 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +TrimBy 0000000b 0 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +TrimBy 0000000a 1 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +TrimBy 00000009 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +TrimBy 0000000c 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddSurface +Curve 00000001 1 2 00000001 00000003 +CCtrl 0 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +CurvePt 0 0.00000000000000000000 4.64894150531215188948 1.84047354780936389673 +CurvePt 0 0.00000000000000000000 3.53553390593273775266 3.53553390593273775266 +CurvePt 0 0.00000000000000000000 1.84047354780936411878 4.64894150531215188948 +CurvePt 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddCurve +Curve 00000002 1 2 00000002 00000003 +CCtrl 0 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +CurvePt 0 10.00000000000000000000 4.64894150531215188948 1.84047354780936389673 +CurvePt 0 10.00000000000000000000 3.53553390593273775266 3.53553390593273775266 +CurvePt 0 10.00000000000000000000 1.84047354780936411878 4.64894150531215188948 +CurvePt 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddCurve +Curve 00000003 1 1 00000003 00000004 +CCtrl 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +CurvePt 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +AddCurve +Curve 00000004 1 2 00000001 00000004 +CCtrl 0 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 -4.99999999999999911182 5.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +CurvePt 0 0.00000000000000000000 -1.84047354780936323060 4.64894150531215188948 +CurvePt 0 0.00000000000000000000 -3.53553390593273775266 3.53553390593273775266 +CurvePt 0 0.00000000000000000000 -4.64894150531215188948 1.84047354780936434082 +CurvePt 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddCurve +Curve 00000005 1 2 00000002 00000004 +CCtrl 0 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -4.99999999999999911182 5.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 +CurvePt 0 10.00000000000000000000 -1.84047354780936323060 4.64894150531215188948 +CurvePt 0 10.00000000000000000000 -3.53553390593273775266 3.53553390593273775266 +CurvePt 0 10.00000000000000000000 -4.64894150531215188948 1.84047354780936434082 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddCurve +Curve 00000006 1 1 00000004 00000005 +CCtrl 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +AddCurve +Curve 00000007 1 2 00000001 00000005 +CCtrl 0 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 -5.00000000000000088818 -4.99999999999999911182 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +CurvePt 0 0.00000000000000000000 -4.64894150531215188948 -1.84047354780936300855 +CurvePt 0 0.00000000000000000000 -3.53553390593273864084 -3.53553390593273730858 +CurvePt 0 0.00000000000000000000 -1.84047354780936478491 -4.64894150531215188948 +CurvePt 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddCurve +Curve 00000008 1 2 00000002 00000005 +CCtrl 0 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -5.00000000000000088818 -4.99999999999999911182 Weight 0.70710678118654757274 +CCtrl 2 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 +CurvePt 0 10.00000000000000000000 -4.64894150531215188948 -1.84047354780936300855 +CurvePt 0 10.00000000000000000000 -3.53553390593273864084 -3.53553390593273730858 +CurvePt 0 10.00000000000000000000 -1.84047354780936478491 -4.64894150531215188948 +CurvePt 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddCurve +Curve 00000009 1 1 00000005 00000006 +CCtrl 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +CurvePt 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +AddCurve +Curve 0000000a 1 2 00000001 00000006 +CCtrl 0 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000000000 4.99999999999999911182 -5.00000000000000088818 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +CurvePt 0 0.00000000000000000000 1.84047354780936300855 -4.64894150531215188948 +CurvePt 0 0.00000000000000000000 3.53553390593273730858 -3.53553390593273864084 +CurvePt 0 0.00000000000000000000 4.64894150531215188948 -1.84047354780936500696 +CurvePt 1 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddCurve +Curve 0000000b 1 2 00000002 00000006 +CCtrl 0 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 4.99999999999999911182 -5.00000000000000088818 Weight 0.70710678118654757274 +CCtrl 2 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 +CurvePt 0 10.00000000000000000000 1.84047354780936300855 -4.64894150531215188948 +CurvePt 0 10.00000000000000000000 3.53553390593273730858 -3.53553390593273864084 +CurvePt 0 10.00000000000000000000 4.64894150531215188948 -1.84047354780936500696 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddCurve +Curve 0000000c 1 1 00000006 00000003 +CCtrl 0 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 +AddCurve diff --git a/test/constraint/pt_face_distance/test.cpp b/test/constraint/pt_face_distance/test.cpp new file mode 100644 index 0000000..7e2a06e --- /dev/null +++ b/test/constraint/pt_face_distance/test.cpp @@ -0,0 +1,33 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(reference_roundtrip) { + CHECK_LOAD("reference.slvs"); + CHECK_RENDER("reference.png"); + CHECK_SAVE("reference.slvs"); +} + +TEST_CASE(reference_migrate_from_v20) { + CHECK_LOAD("reference_v20.slvs"); + CHECK_SAVE("reference.slvs"); +} + +TEST_CASE(reference_migrate_from_v22) { + CHECK_LOAD("reference_v22.slvs"); + CHECK_SAVE("reference.slvs"); +} diff --git a/test/constraint/pt_in_plane/normal.png b/test/constraint/pt_in_plane/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..b770451f3a56df7288f51e1c10d35646b2135cb7 GIT binary patch literal 4228 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%0Dr5 zm#d|7B>i+ik;R>5^B=8*)|0^n8d%l)o@FuKyMh?vtn87Wh!NCItWP@r zuZnhhjMzLl^kQNA3kR@&b7}2xQgUza-lg6ieM)^{CY$izFXAMqv^;pYk0QpFB7eld NCYLQ6ve%Oj{R_kdMsffE literal 0 HcmV?d00001 diff --git a/test/constraint/pt_line_distance/extended.slvs b/test/constraint/pt_line_distance/extended.slvs new file mode 100644 index 0000000..c73570d --- /dev/null +++ b/test/constraint/pt_line_distance/extended.slvs @@ -0,0 +1,316 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=10.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=32 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=-5.00000000000000000000 +Constraint.ptA.v=00050000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +Constraint.disp.offset.x=-10.00000000000000000000 +Constraint.disp.offset.y=0.0000000000000000000 +AddConstraint + diff --git a/test/constraint/pt_line_distance/free_in_3d.png b/test/constraint/pt_line_distance/free_in_3d.png new file mode 100644 index 0000000000000000000000000000000000000000..db351dfde01e033f02d4a60e598aa85385f99d74 GIT binary patch literal 4403 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 literal 0 HcmV?d00001 diff --git a/test/constraint/pt_on_circle/normal.slvs b/test/constraint/pt_on_circle/normal.slvs new file mode 100644 index 0000000..462e0cd --- /dev/null +++ b/test/constraint/pt_on_circle/normal.slvs @@ -0,0 +1,318 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-9.99999912024158099655 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040040 +Param.val=4.99999824048316199310 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000087975841900345 +AddParam + +Param.h.v.=00050011 +Param.val=10.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-9.99999912024158099655 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=4.99999824048316199310 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000087975841900345 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=100 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/pt_on_circle/normal_v20.slvs b/test/constraint/pt_on_circle/normal_v20.slvs new file mode 100644 index 0000000..d36fc3b --- /dev/null +++ b/test/constraint/pt_on_circle/normal_v20.slvs @@ -0,0 +1,318 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-9.99999912024158100000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040040 +Param.val=4.99999824048316200000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000087975841900000 +AddParam + +Param.h.v.=00050011 +Param.val=10.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-9.99999912024158100000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=4.99999824048316200000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000087975841900000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=100 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/pt_on_circle/normal_v22.slvs b/test/constraint/pt_on_circle/normal_v22.slvs new file mode 100644 index 0000000..fea01e6 --- /dev/null +++ b/test/constraint/pt_on_circle/normal_v22.slvs @@ -0,0 +1,318 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-9.99999912024158100000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040040 +Param.val=4.99999824048316200000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000087975841900000 +AddParam + +Param.h.v.=00050011 +Param.val=10.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-9.99999912024158100000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=4.99999824048316200000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000087975841900000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=100 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/pt_on_circle/test.cpp b/test/constraint/pt_on_circle/test.cpp new file mode 100644 index 0000000..3b4c470 --- /dev/null +++ b/test/constraint/pt_on_circle/test.cpp @@ -0,0 +1,25 @@ +#include "solvespace.h" + +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(negative_dia) { + CHECK_LOAD("negative_dia.slvs"); + Entity *e = SK.GetEntity(hRequest { 4 }.entity(0)); + CHECK_TRUE(e->CircleGetRadiusNum() > 0); +} diff --git a/test/constraint/pt_on_face/normal.png b/test/constraint/pt_on_face/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..0d6ef8215d12540e19517c40006ac4f53ed8239f GIT binary patch literal 4662 zcmeHLdpMM78-M0CqhTnA9KLEPV%top&9V$85!Pv2Vw^`}TeK6tXh!i;tx#-bQ9?)O zVQP0A7ca^r6^db1%&xC>n1pqhgz>%7woT1;eb-*s_kI8DT<=`(T=Txq^W69Eci;E( zyPt&59j#@hS4jf^WNmFMzW@NDEPnBl=s&fA8IJ&vpSQK#=5p}-$m4Bc4{gqU^Rs2V zUi<1Hh462rO9x~W2X!3$pSAH5jko(+^xWBOl(ed9ug4|wkY@Dy-{=~uJIey}t`Mu-CROZ= z^OUqfFTcn_3pU$f;^uwfQ~QLc$)B)Nw5tppnTEnA!Z8blx6lU*kW(2VUcF5_+5Fc^1`ZU7dVvDF-^(h>5GMlWf!HU} zp8#arS#dMAr34iCVA?V-O?)E)Zb%R_t55|4_OBPqUMxoH8n^ad z2a$-ScvCBLWWoN=#m+eC?_THzCUXaWTqgo9)yoz5d|Mk^K zJ=EP$N1N-32`Q7NYQ7HLVqI2ZNdhz*X`Iy?JvWg<6OxeL?jyKV+neoCoU_`7d~6V& zNOc~)*)}mT5nm^mD({+}NT7Zn3M1y3Xmt30OP z(UcOfY49b6_w@k&r!B?YN4e{sklMH0 zh0GeuZ)2W?(FK#ed9LN>%?%@ymYHp*Vv0T(I?Y?v+S+PWdNcj+boa^>*+@mpL7%Cb(x8cN{ z$-?9=wI;Owk5O|e`f9PbBjY#Kc;l!Q4O)c^p$v7+Qasg@z0`{P+40x#L|C@c9`BE? z4tzT>Wm-!Ki0pXU7#E5ssvgyQmLhn_51vwhv*ajXT@1;T-RI+bO!;XuDV@YbApDE| zp&{$NHJr@lyyuHym=RdtUAs#U94zk5(<)u4!ly0>XE;s^GcK%4Q@Wb`l)S^GO1BC# zsY>38IOFMZ;l0)A=`s%AQ`ew-a0r4WG<^}3Fs9>N9MG&jd)eQ(wm(bNv{*b|?pMP?A4<(;HuSU}!YjCugTEWq zzK)6?s;1FGmI)qVWdF%;gHdPss* zz%ZY4M^sUkZfI?U_bqaA+*36PiQ87O703D@7RQo5Md(Kxy~=2~gFwVtt^WJw%Z7KYiV_^wSf0ONkz{#hp$n-F%`2OOV`i z2^yoIZv-~{W;D>{dZ!@+UH-O;kTbEWu>$>hZ>3Iaur#_*H@)G$og8Ui7&WEf`f*KM z)~jeVTK8?IT``oR#e%u#^lezzJ}^S-b!57y!*%3~j;{~UTnb}ZMG+)zG@1BWwn3$h&Z z)cx-B1Gf9&)-Tqid)w~&@0HTw$q(zwE2AN+Ht^#5&)ldVp84e{joD~Z$d8^_7O~4R z3%CcKCT!>&#ZSm~i^cSV$6&5geyo3|ph_YM1yIv3>gois}#W!>;5*W$Rh+-5MoN zW&lmFHXr^BB5XLIb5ysV-`huM^9bJO{#iEn_dUtMXoRBM-5bDu!sa@5CIMii zAb!Z7cY1L2o$x7XxOe3nebs6`mLsZmKyryLYop+c<3zxVZOTL!B)!`;iUXu)Z~0?V zOqui0MgU8E{Qr*7Oyj>|ib;6T<;NcCAlaHalG+w$_ynsSLcTPxCeG76Nyq%C#i+r_&B zb>zh2ofgzPQl~RuE)$9a^I_`6S(U2^6#{ORGcKYQ$yQ2V@?0QvvIt6OQEj2FFyD-4yS_;C@J8h{5+Ac;5 zsNS3|=ITTUgyi=jjFejtNSyiyWz^gFpP$Nu3YQK^=h=y9*@xfPNmry#NTMcxi1yMZ zt|s*_Zn+7s4nIeQ$*arviY}VH%WrF0{7+9r{^S5=)?B@N$U}>`We0j51Z=GwElVt@ GasLL6s(|+Z literal 0 HcmV?d00001 diff --git a/test/constraint/pt_on_face/normal.slvs b/test/constraint/pt_on_face/normal.slvs new file mode 100644 index 0000000..7656071 --- /dev/null +++ b/test/constraint/pt_on_face/normal.slvs @@ -0,0 +1,703 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000003 +Group.type=5100 +Group.order=2 +Group.name=extrude +Group.activeWorkplane.v=00010000 +Group.opA.v=00000002 +Group.color=00646464 +Group.subtype=7000 +Group.skipFirst=0 +Group.predef.entityB.v=80020000 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ + 1 00040000 1002 + 2 00040001 1002 + 3 00040020 1002 + 4 00040040 1002 + 5 00040000 1001 + 6 00040001 1001 + 7 00040020 1001 + 8 00040040 1001 + 9 00040001 1003 + 10 80020000 1002 + 11 80020000 1001 + 12 80020001 1002 + 13 80020002 1002 + 14 80020001 1001 + 15 80020002 1001 + 16 80020002 1003 + 17 00000000 1001 + 18 00000000 1002 +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +AddParam + +Param.h.v.=00040011 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Param.h.v.=80030000 +AddParam + +Param.h.v.=80030001 +AddParam + +Param.h.v.=80030002 +Param.val=-5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.group.v=00000003 +Request.construction=1 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2000 +Entity.construction=1 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=0 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030001 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.normal.v=80030003 +Entity.distance.v=80030004 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030002 +Entity.type=2010 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030003 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030004 +Entity.type=4001 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030005 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.normal.v=80030007 +Entity.distance.v=80030008 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030006 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.z=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030007 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030008 +Entity.type=4001 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030009 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.point[1].v=80030002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000c +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003000d +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000d +Entity.type=2010 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000e +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000f +Entity.type=2010 +Entity.construction=1 +Entity.actPoint.z=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030010 +Entity.type=11000 +Entity.construction=1 +Entity.point[0].v=8003000f +Entity.point[1].v=8003000d +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030011 +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.actPoint.z=-10.00000000000000000000 +Entity.actNormal.vz=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030012 +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003000d +Entity.actNormal.vz=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00010001 +Constraint.ptB.v=00040001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=43 +Constraint.group.v=00000003 +Constraint.ptA.v=00050000 +Constraint.entityA.v=80030012 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Surface 00000001 00646464 80030011 1 1 +SCtrl 0 0 -5.00000000000000000000 -5.00000000000000088818 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 5.00000000010000000827 -5.00000000000000088818 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 -5.00000000000000000000 4.99999999999999911182 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 5.00000000010000000827 4.99999999999999911182 -10.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000a 0 -0.00000000000000177636 -5.00000000000000088818 -10.00000000000000000000 5.00000000000000000000 -0.00000000000000088818 -10.00000000000000000000 +TrimBy 00000001 0 5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 0.00000000000000000000 4.99999999999999911182 -10.00000000000000000000 +TrimBy 00000004 0 0.00000000000000000000 4.99999999999999911182 -10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 +TrimBy 00000007 0 -5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 -0.00000000000000177636 -5.00000000000000088818 -10.00000000000000000000 +AddSurface +Surface 00000002 00646464 80030012 1 1 +SCtrl 0 0 5.00000000010000000827 -5.00000000000000088818 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 -5.00000000000000000000 -5.00000000000000088818 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 5.00000000010000000827 4.99999999999999911182 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 -5.00000000000000000000 4.99999999999999911182 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000005 1 -5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 0.00000000000000000000 4.99999999999999911182 0.00000000000000000000 +TrimBy 00000002 1 0.00000000000000000000 4.99999999999999911182 0.00000000000000000000 5.00000000000000088818 -0.00000000000000000000 0.00000000000000000000 +TrimBy 0000000b 1 5.00000000000000088818 -0.00000000000000088818 0.00000000000000000000 -0.00000000000000088818 -5.00000000000000088818 0.00000000000000000000 +TrimBy 00000008 1 -0.00000000000000088818 -5.00000000000000088818 0.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 +AddSurface +Surface 00000003 00646464 00000000 2 1 +SCtrl 0 0 5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 5.00000000000000000000 5.00000000000000000000 -10.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 1 1 5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 2 0 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000002 0 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 +TrimBy 00000001 1 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 +TrimBy 0000000c 0 5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 +TrimBy 00000003 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 +AddSurface +Surface 00000004 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 -4.99999999999999911182 5.00000000000000000000 -10.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 1 1 -4.99999999999999911182 5.00000000000000000000 0.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 2 0 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000005 0 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 +TrimBy 00000004 1 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 +TrimBy 00000003 0 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 +TrimBy 00000006 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 +AddSurface +Surface 00000005 00646464 00000000 2 1 +SCtrl 0 0 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 -5.00000000000000088818 -4.99999999999999911182 -10.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 1 1 -5.00000000000000088818 -4.99999999999999911182 0.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 2 0 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000008 0 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 +TrimBy 00000007 1 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 +TrimBy 00000006 0 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 +TrimBy 00000009 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 +AddSurface +Surface 00000006 00646464 00000000 2 1 +SCtrl 0 0 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 4.99999999999999911182 -5.00000000000000088818 -10.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 1 1 4.99999999999999911182 -5.00000000000000088818 0.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 2 0 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000b 0 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 +TrimBy 0000000a 1 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 +TrimBy 00000009 0 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 +TrimBy 0000000c 1 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 +AddSurface +Curve 00000001 1 2 00000001 00000003 +CCtrl 0 5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 5.00000000000000000000 -10.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 +CurvePt 0 4.91652684208287205081 0.90981526206072360630 -10.00000000000000000000 +CurvePt 0 4.64894150531215188948 1.84047354780936389673 -10.00000000000000000000 +CurvePt 0 4.18497755609325672310 2.73604876692571252761 -10.00000000000000000000 +CurvePt 0 3.53553390593273775266 3.53553390593273775266 -10.00000000000000000000 +CurvePt 0 2.73604876692571252761 4.18497755609325672310 -10.00000000000000000000 +CurvePt 0 1.84047354780936411878 4.64894150531215188948 -10.00000000000000000000 +CurvePt 0 0.90981526206072382834 4.91652684208287205081 -10.00000000000000000000 +CurvePt 1 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 +AddCurve +Curve 00000002 1 2 00000002 00000003 +CCtrl 0 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 +CurvePt 0 4.91652684208287205081 0.90981526206072360630 0.00000000000000000000 +CurvePt 0 4.64894150531215188948 1.84047354780936389673 0.00000000000000000000 +CurvePt 0 4.18497755609325672310 2.73604876692571252761 0.00000000000000000000 +CurvePt 0 3.53553390593273775266 3.53553390593273775266 0.00000000000000000000 +CurvePt 0 2.73604876692571252761 4.18497755609325672310 0.00000000000000000000 +CurvePt 0 1.84047354780936411878 4.64894150531215188948 0.00000000000000000000 +CurvePt 0 0.90981526206072382834 4.91652684208287205081 0.00000000000000000000 +CurvePt 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000003 1 1 00000003 00000004 +CCtrl 0 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 +CurvePt 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000004 1 2 00000001 00000004 +CCtrl 0 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -4.99999999999999911182 5.00000000000000000000 -10.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 +CurvePt 0 -0.90981526206072327323 4.91652684208287205081 -10.00000000000000000000 +CurvePt 0 -1.84047354780936323060 4.64894150531215188948 -10.00000000000000000000 +CurvePt 0 -2.73604876692571208352 4.18497755609325672310 -10.00000000000000000000 +CurvePt 0 -3.53553390593273775266 3.53553390593273775266 -10.00000000000000000000 +CurvePt 0 -4.18497755609325672310 2.73604876692571297170 -10.00000000000000000000 +CurvePt 0 -4.64894150531215188948 1.84047354780936434082 -10.00000000000000000000 +CurvePt 0 -4.91652684208287205081 0.90981526206072405039 -10.00000000000000000000 +CurvePt 1 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 +AddCurve +Curve 00000005 1 2 00000002 00000004 +CCtrl 0 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -4.99999999999999911182 5.00000000000000000000 0.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 +CurvePt 0 -0.90981526206072327323 4.91652684208287205081 0.00000000000000000000 +CurvePt 0 -1.84047354780936323060 4.64894150531215188948 0.00000000000000000000 +CurvePt 0 -2.73604876692571208352 4.18497755609325672310 0.00000000000000000000 +CurvePt 0 -3.53553390593273775266 3.53553390593273775266 0.00000000000000000000 +CurvePt 0 -4.18497755609325672310 2.73604876692571297170 0.00000000000000000000 +CurvePt 0 -4.64894150531215188948 1.84047354780936434082 0.00000000000000000000 +CurvePt 0 -4.91652684208287205081 0.90981526206072405039 0.00000000000000000000 +CurvePt 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 +AddCurve +Curve 00000006 1 1 00000004 00000005 +CCtrl 0 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 +CurvePt 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 +AddCurve +Curve 00000007 1 2 00000001 00000005 +CCtrl 0 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000088818 -4.99999999999999911182 -10.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 +CurvePt 0 -4.91652684208287205081 -0.90981526206072305119 -10.00000000000000000000 +CurvePt 0 -4.64894150531215188948 -1.84047354780936300855 -10.00000000000000000000 +CurvePt 0 -4.18497755609325672310 -2.73604876692571208352 -10.00000000000000000000 +CurvePt 0 -3.53553390593273864084 -3.53553390593273730858 -10.00000000000000000000 +CurvePt 0 -2.73604876692571297170 -4.18497755609325672310 -10.00000000000000000000 +CurvePt 0 -1.84047354780936478491 -4.64894150531215188948 -10.00000000000000000000 +CurvePt 0 -0.90981526206072438345 -4.91652684208287205081 -10.00000000000000000000 +CurvePt 1 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 +AddCurve +Curve 00000008 1 2 00000002 00000005 +CCtrl 0 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000088818 -4.99999999999999911182 0.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 +CurvePt 0 -4.91652684208287205081 -0.90981526206072305119 0.00000000000000000000 +CurvePt 0 -4.64894150531215188948 -1.84047354780936300855 0.00000000000000000000 +CurvePt 0 -4.18497755609325672310 -2.73604876692571208352 0.00000000000000000000 +CurvePt 0 -3.53553390593273864084 -3.53553390593273730858 0.00000000000000000000 +CurvePt 0 -2.73604876692571297170 -4.18497755609325672310 0.00000000000000000000 +CurvePt 0 -1.84047354780936478491 -4.64894150531215188948 0.00000000000000000000 +CurvePt 0 -0.90981526206072438345 -4.91652684208287205081 0.00000000000000000000 +CurvePt 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000009 1 1 00000005 00000006 +CCtrl 0 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 +CurvePt 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 0000000a 1 2 00000001 00000006 +CCtrl 0 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 4.99999999999999911182 -5.00000000000000088818 -10.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 +CurvePt 0 0.90981526206072282914 -4.91652684208287205081 -10.00000000000000000000 +CurvePt 0 1.84047354780936300855 -4.64894150531215188948 -10.00000000000000000000 +CurvePt 0 2.73604876692571163943 -4.18497755609325672310 -10.00000000000000000000 +CurvePt 0 3.53553390593273730858 -3.53553390593273864084 -10.00000000000000000000 +CurvePt 0 4.18497755609325583492 -2.73604876692571297170 -10.00000000000000000000 +CurvePt 0 4.64894150531215188948 -1.84047354780936500696 -10.00000000000000000000 +CurvePt 0 4.91652684208287205081 -0.90981526206072471652 -10.00000000000000000000 +CurvePt 1 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 +AddCurve +Curve 0000000b 1 2 00000002 00000006 +CCtrl 0 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 4.99999999999999911182 -5.00000000000000088818 0.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 +CurvePt 0 0.90981526206072282914 -4.91652684208287205081 0.00000000000000000000 +CurvePt 0 1.84047354780936300855 -4.64894150531215188948 0.00000000000000000000 +CurvePt 0 2.73604876692571163943 -4.18497755609325672310 0.00000000000000000000 +CurvePt 0 3.53553390593273730858 -3.53553390593273864084 0.00000000000000000000 +CurvePt 0 4.18497755609325583492 -2.73604876692571297170 0.00000000000000000000 +CurvePt 0 4.64894150531215188948 -1.84047354780936500696 0.00000000000000000000 +CurvePt 0 4.91652684208287205081 -0.90981526206072471652 0.00000000000000000000 +CurvePt 1 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 +AddCurve +Curve 0000000c 1 1 00000006 00000003 +CCtrl 0 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 +CurvePt 1 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 +AddCurve diff --git a/test/constraint/pt_on_face/normal_v20.slvs b/test/constraint/pt_on_face/normal_v20.slvs new file mode 100644 index 0000000..e3c7212 --- /dev/null +++ b/test/constraint/pt_on_face/normal_v20.slvs @@ -0,0 +1,670 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000003 +Group.type=5100 +Group.order=2 +Group.name=extrude +Group.activeWorkplane.v=00010000 +Group.opA.v=00000002 +Group.color=00646464 +Group.subtype=7000 +Group.skipFirst=0 +Group.predef.entityB.v=80020000 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ + 1 00040000 1002 + 2 00040001 1002 + 3 00040020 1002 + 4 00040040 1002 + 5 00040000 1001 + 6 00040001 1001 + 7 00040020 1001 + 8 00040040 1001 + 9 00040001 1003 + 10 80020000 1002 + 11 80020000 1001 + 12 80020001 1002 + 13 80020002 1002 + 14 80020001 1001 + 15 80020002 1001 + 16 80020002 1003 + 17 00000000 1001 + 18 00000000 1002 +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +AddParam + +Param.h.v.=00040011 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Param.h.v.=80030000 +AddParam + +Param.h.v.=80030001 +AddParam + +Param.h.v.=80030002 +Param.val=-5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.group.v=00000003 +Request.construction=1 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=0 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030001 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.normal.v=80030003 +Entity.distance.v=80030004 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030002 +Entity.type=2010 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030003 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030004 +Entity.type=4001 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030005 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.normal.v=80030007 +Entity.distance.v=80030008 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030006 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.z=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030007 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030008 +Entity.type=4001 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030009 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.point[1].v=80030002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000c +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003000d +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000d +Entity.type=2010 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000e +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000f +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.z=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030010 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.point[1].v=8003000d +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030011 +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.actPoint.z=-10.00000000000000000000 +Entity.actNormal.vz=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030012 +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003000d +Entity.actNormal.vz=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00010001 +Constraint.ptB.v=00040001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=43 +Constraint.group.v=00000003 +Constraint.ptA.v=00050000 +Constraint.entityA.v=80030012 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Surface 00000001 00646464 80030011 1 1 +SCtrl 0 0 -5.00000000000000000000 -5.00000000000000090000 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 5.00000000010000000000 -5.00000000000000090000 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 -5.00000000000000000000 4.99999999999999910000 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 5.00000000010000000000 4.99999999999999910000 -10.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000a 0 -0.00000000000000177636 -5.00000000000000090000 -10.00000000000000000000 5.00000000000000000000 -0.00000000000000088818 -10.00000000000000000000 +TrimBy 00000001 0 5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 0.00000000000000000000 4.99999999999999910000 -10.00000000000000000000 +TrimBy 00000004 0 0.00000000000000000000 4.99999999999999910000 -10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 +TrimBy 00000007 0 -5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 -0.00000000000000177636 -5.00000000000000090000 -10.00000000000000000000 +AddSurface +Surface 00000002 00646464 80030012 1 1 +SCtrl 0 0 5.00000000010000000000 -5.00000000000000090000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 -5.00000000000000000000 -5.00000000000000090000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 5.00000000010000000000 4.99999999999999910000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 -5.00000000000000000000 4.99999999999999910000 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000005 1 -5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 0.00000000000000000000 4.99999999999999910000 0.00000000000000000000 +TrimBy 00000002 1 0.00000000000000000000 4.99999999999999910000 0.00000000000000000000 5.00000000000000090000 -0.00000000000000000000 0.00000000000000000000 +TrimBy 0000000b 1 5.00000000000000090000 -0.00000000000000088818 0.00000000000000000000 -0.00000000000000088818 -5.00000000000000090000 0.00000000000000000000 +TrimBy 00000008 1 -0.00000000000000088818 -5.00000000000000090000 0.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 +AddSurface +Surface 00000003 00646464 00000000 2 1 +SCtrl 0 0 5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 5.00000000000000000000 5.00000000000000000000 -10.00000000000000000000 Weight 0.70710678118654757000 +SCtrl 1 1 5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 0.70710678118654757000 +SCtrl 2 0 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000002 0 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 +TrimBy 00000003 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 +TrimBy 00000001 1 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 +TrimBy 0000000c 0 5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 +AddSurface +Surface 00000004 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 -4.99999999999999910000 5.00000000000000000000 -10.00000000000000000000 Weight 0.70710678118654757000 +SCtrl 1 1 -4.99999999999999910000 5.00000000000000000000 0.00000000000000000000 Weight 0.70710678118654757000 +SCtrl 2 0 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000005 0 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 +TrimBy 00000006 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 +TrimBy 00000004 1 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 +TrimBy 00000003 0 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 +AddSurface +Surface 00000005 00646464 00000000 2 1 +SCtrl 0 0 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 -5.00000000000000090000 -4.99999999999999910000 -10.00000000000000000000 Weight 0.70710678118654757000 +SCtrl 1 1 -5.00000000000000090000 -4.99999999999999910000 0.00000000000000000000 Weight 0.70710678118654757000 +SCtrl 2 0 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000008 0 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 +TrimBy 00000009 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 +TrimBy 00000007 1 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 +TrimBy 00000006 0 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 +AddSurface +Surface 00000006 00646464 00000000 2 1 +SCtrl 0 0 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 4.99999999999999910000 -5.00000000000000090000 -10.00000000000000000000 Weight 0.70710678118654757000 +SCtrl 1 1 4.99999999999999910000 -5.00000000000000090000 0.00000000000000000000 Weight 0.70710678118654757000 +SCtrl 2 0 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000b 0 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 +TrimBy 0000000c 1 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 +TrimBy 0000000a 1 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 +TrimBy 00000009 0 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 +AddSurface +Curve 00000001 1 2 00000001 00000003 +CCtrl 0 5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 5.00000000000000000000 -10.00000000000000000000 Weight 0.70710678118654757000 +CCtrl 2 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 +CurvePt 0 4.64894150531215190000 1.84047354780936390000 -10.00000000000000000000 +CurvePt 0 3.53553390593273780000 3.53553390593273780000 -10.00000000000000000000 +CurvePt 0 1.84047354780936410000 4.64894150531215190000 -10.00000000000000000000 +CurvePt 1 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 +AddCurve +Curve 00000002 1 2 00000002 00000003 +CCtrl 0 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 0.70710678118654757000 +CCtrl 2 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 +CurvePt 0 4.64894150531215190000 1.84047354780936390000 0.00000000000000000000 +CurvePt 0 3.53553390593273780000 3.53553390593273780000 0.00000000000000000000 +CurvePt 0 1.84047354780936410000 4.64894150531215190000 0.00000000000000000000 +CurvePt 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000003 1 1 00000003 00000004 +CCtrl 0 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 +CurvePt 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000004 1 2 00000001 00000004 +CCtrl 0 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -4.99999999999999910000 5.00000000000000000000 -10.00000000000000000000 Weight 0.70710678118654757000 +CCtrl 2 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 +CurvePt 0 -1.84047354780936320000 4.64894150531215190000 -10.00000000000000000000 +CurvePt 0 -3.53553390593273780000 3.53553390593273780000 -10.00000000000000000000 +CurvePt 0 -4.64894150531215190000 1.84047354780936430000 -10.00000000000000000000 +CurvePt 1 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 +AddCurve +Curve 00000005 1 2 00000002 00000004 +CCtrl 0 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -4.99999999999999910000 5.00000000000000000000 0.00000000000000000000 Weight 0.70710678118654757000 +CCtrl 2 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 +CurvePt 0 -1.84047354780936320000 4.64894150531215190000 0.00000000000000000000 +CurvePt 0 -3.53553390593273780000 3.53553390593273780000 0.00000000000000000000 +CurvePt 0 -4.64894150531215190000 1.84047354780936430000 0.00000000000000000000 +CurvePt 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 +AddCurve +Curve 00000006 1 1 00000004 00000005 +CCtrl 0 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 +CurvePt 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 +AddCurve +Curve 00000007 1 2 00000001 00000005 +CCtrl 0 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000090000 -4.99999999999999910000 -10.00000000000000000000 Weight 0.70710678118654757000 +CCtrl 2 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 +CurvePt 0 -4.64894150531215190000 -1.84047354780936300000 -10.00000000000000000000 +CurvePt 0 -3.53553390593273860000 -3.53553390593273730000 -10.00000000000000000000 +CurvePt 0 -1.84047354780936480000 -4.64894150531215190000 -10.00000000000000000000 +CurvePt 1 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 +AddCurve +Curve 00000008 1 2 00000002 00000005 +CCtrl 0 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000090000 -4.99999999999999910000 0.00000000000000000000 Weight 0.70710678118654757000 +CCtrl 2 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 +CurvePt 0 -4.64894150531215190000 -1.84047354780936300000 0.00000000000000000000 +CurvePt 0 -3.53553390593273860000 -3.53553390593273730000 0.00000000000000000000 +CurvePt 0 -1.84047354780936480000 -4.64894150531215190000 0.00000000000000000000 +CurvePt 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000009 1 1 00000005 00000006 +CCtrl 0 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 +CurvePt 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 0000000a 1 2 00000001 00000006 +CCtrl 0 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 4.99999999999999910000 -5.00000000000000090000 -10.00000000000000000000 Weight 0.70710678118654757000 +CCtrl 2 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 +CurvePt 0 1.84047354780936300000 -4.64894150531215190000 -10.00000000000000000000 +CurvePt 0 3.53553390593273730000 -3.53553390593273860000 -10.00000000000000000000 +CurvePt 0 4.64894150531215190000 -1.84047354780936500000 -10.00000000000000000000 +CurvePt 1 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 +AddCurve +Curve 0000000b 1 2 00000002 00000006 +CCtrl 0 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 4.99999999999999910000 -5.00000000000000090000 0.00000000000000000000 Weight 0.70710678118654757000 +CCtrl 2 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 +CurvePt 0 1.84047354780936300000 -4.64894150531215190000 0.00000000000000000000 +CurvePt 0 3.53553390593273730000 -3.53553390593273860000 0.00000000000000000000 +CurvePt 0 4.64894150531215190000 -1.84047354780936500000 0.00000000000000000000 +CurvePt 1 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 +AddCurve +Curve 0000000c 1 1 00000006 00000003 +CCtrl 0 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 +CurvePt 1 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 +AddCurve diff --git a/test/constraint/pt_on_face/normal_v22.slvs b/test/constraint/pt_on_face/normal_v22.slvs new file mode 100644 index 0000000..03ee428 --- /dev/null +++ b/test/constraint/pt_on_face/normal_v22.slvs @@ -0,0 +1,687 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000003 +Group.type=5100 +Group.order=2 +Group.name=extrude +Group.activeWorkplane.v=00010000 +Group.opA.v=00000002 +Group.color=00646464 +Group.subtype=7000 +Group.skipFirst=0 +Group.predef.entityB.v=80020000 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ + 1 00040000 1002 + 2 00040001 1002 + 3 00040020 1002 + 4 00040040 1002 + 5 00040000 1001 + 6 00040001 1001 + 7 00040020 1001 + 8 00040040 1001 + 9 00040001 1003 + 10 80020000 1002 + 11 80020000 1001 + 12 80020001 1002 + 13 80020002 1002 + 14 80020001 1001 + 15 80020002 1001 + 16 80020002 1003 + 17 00000000 1001 + 18 00000000 1002 +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +AddParam + +Param.h.v.=00040011 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Param.h.v.=80030000 +AddParam + +Param.h.v.=80030001 +AddParam + +Param.h.v.=80030002 +Param.val=-5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.group.v=00000003 +Request.construction=1 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=0 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030001 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.normal.v=80030003 +Entity.distance.v=80030004 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030002 +Entity.type=2010 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030003 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030004 +Entity.type=4001 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030005 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.normal.v=80030007 +Entity.distance.v=80030008 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030006 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.z=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030007 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030008 +Entity.type=4001 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030009 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.point[1].v=80030002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000c +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003000d +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000d +Entity.type=2010 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000e +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000f +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.z=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030010 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.point[1].v=8003000d +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030011 +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.actPoint.z=-10.00000000000000000000 +Entity.actNormal.vz=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030012 +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003000d +Entity.actNormal.vz=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00010001 +Constraint.ptB.v=00040001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=43 +Constraint.group.v=00000003 +Constraint.ptA.v=00050000 +Constraint.entityA.v=80030012 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Surface 00000001 00646464 80030011 1 1 +SCtrl 0 0 -5.00000000000000000000 -5.00000000000000088818 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 5.00000000010000000827 -5.00000000000000088818 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 -5.00000000000000000000 4.99999999999999911182 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 5.00000000010000000827 4.99999999999999911182 -10.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000a 0 -0.00000000000000177636 -5.00000000000000088818 -10.00000000000000000000 5.00000000000000000000 -0.00000000000000088818 -10.00000000000000000000 +TrimBy 00000001 0 5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 0.00000000000000000000 4.99999999999999911182 -10.00000000000000000000 +TrimBy 00000004 0 0.00000000000000000000 4.99999999999999911182 -10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 +TrimBy 00000007 0 -5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 -0.00000000000000177636 -5.00000000000000088818 -10.00000000000000000000 +AddSurface +Surface 00000002 00646464 80030012 1 1 +SCtrl 0 0 5.00000000010000000827 -5.00000000000000088818 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 -5.00000000000000000000 -5.00000000000000088818 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 5.00000000010000000827 4.99999999999999911182 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 -5.00000000000000000000 4.99999999999999911182 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000005 1 -5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 0.00000000000000000000 4.99999999999999911182 0.00000000000000000000 +TrimBy 00000002 1 0.00000000000000000000 4.99999999999999911182 0.00000000000000000000 5.00000000000000088818 -0.00000000000000000000 0.00000000000000000000 +TrimBy 0000000b 1 5.00000000000000088818 -0.00000000000000088818 0.00000000000000000000 -0.00000000000000088818 -5.00000000000000088818 0.00000000000000000000 +TrimBy 00000008 1 -0.00000000000000088818 -5.00000000000000088818 0.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 +AddSurface +Surface 00000003 00646464 00000000 2 1 +SCtrl 0 0 5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 5.00000000000000000000 5.00000000000000000000 -10.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 1 1 5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 2 0 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000002 0 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 +TrimBy 00000001 1 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 +TrimBy 0000000c 0 5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 +TrimBy 00000003 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 +AddSurface +Surface 00000004 00646464 00000000 2 1 +SCtrl 0 0 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 -4.99999999999999911182 5.00000000000000000000 -10.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 1 1 -4.99999999999999911182 5.00000000000000000000 0.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 2 0 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000005 0 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 +TrimBy 00000004 1 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 +TrimBy 00000003 0 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 +TrimBy 00000006 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 +AddSurface +Surface 00000005 00646464 00000000 2 1 +SCtrl 0 0 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 -5.00000000000000088818 -4.99999999999999911182 -10.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 1 1 -5.00000000000000088818 -4.99999999999999911182 0.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 2 0 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000008 0 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 +TrimBy 00000007 1 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 +TrimBy 00000006 0 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 +TrimBy 00000009 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 +AddSurface +Surface 00000006 00646464 00000000 2 1 +SCtrl 0 0 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 4.99999999999999911182 -5.00000000000000088818 -10.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 1 1 4.99999999999999911182 -5.00000000000000088818 0.00000000000000000000 Weight 0.70710678118654757274 +SCtrl 2 0 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 2 1 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000b 0 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 +TrimBy 0000000a 1 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 +TrimBy 00000009 0 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 +TrimBy 0000000c 1 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 +AddSurface +Curve 00000001 1 2 00000001 00000003 +CCtrl 0 5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 5.00000000000000000000 -10.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 0.00000000000000000000 -10.00000000000000000000 +CurvePt 0 4.64894150531215188948 1.84047354780936389673 -10.00000000000000000000 +CurvePt 0 4.18497755609325672310 2.73604876692571252761 -10.00000000000000000000 +CurvePt 0 3.53553390593273775266 3.53553390593273775266 -10.00000000000000000000 +CurvePt 0 2.73604876692571252761 4.18497755609325672310 -10.00000000000000000000 +CurvePt 0 1.84047354780936411878 4.64894150531215188948 -10.00000000000000000000 +CurvePt 1 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 +AddCurve +Curve 00000002 1 2 00000002 00000003 +CCtrl 0 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 0.00000000000000000000 0.00000000000000000000 +CurvePt 0 4.64894150531215188948 1.84047354780936389673 0.00000000000000000000 +CurvePt 0 4.18497755609325672310 2.73604876692571252761 0.00000000000000000000 +CurvePt 0 3.53553390593273775266 3.53553390593273775266 0.00000000000000000000 +CurvePt 0 2.73604876692571252761 4.18497755609325672310 0.00000000000000000000 +CurvePt 0 1.84047354780936411878 4.64894150531215188948 0.00000000000000000000 +CurvePt 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000003 1 1 00000003 00000004 +CCtrl 0 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 +CurvePt 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000004 1 2 00000001 00000004 +CCtrl 0 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -4.99999999999999911182 5.00000000000000000000 -10.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000030616 5.00000000000000000000 -10.00000000000000000000 +CurvePt 0 -1.84047354780936323060 4.64894150531215188948 -10.00000000000000000000 +CurvePt 0 -2.73604876692571208352 4.18497755609325672310 -10.00000000000000000000 +CurvePt 0 -3.53553390593273775266 3.53553390593273775266 -10.00000000000000000000 +CurvePt 0 -4.18497755609325672310 2.73604876692571297170 -10.00000000000000000000 +CurvePt 0 -4.64894150531215188948 1.84047354780936434082 -10.00000000000000000000 +CurvePt 1 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 +AddCurve +Curve 00000005 1 2 00000002 00000004 +CCtrl 0 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -4.99999999999999911182 5.00000000000000000000 0.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 0.00000000000000030616 5.00000000000000000000 0.00000000000000000000 +CurvePt 0 -1.84047354780936323060 4.64894150531215188948 0.00000000000000000000 +CurvePt 0 -2.73604876692571208352 4.18497755609325672310 0.00000000000000000000 +CurvePt 0 -3.53553390593273775266 3.53553390593273775266 0.00000000000000000000 +CurvePt 0 -4.18497755609325672310 2.73604876692571297170 0.00000000000000000000 +CurvePt 0 -4.64894150531215188948 1.84047354780936434082 0.00000000000000000000 +CurvePt 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 +AddCurve +Curve 00000006 1 1 00000004 00000005 +CCtrl 0 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 +CurvePt 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 +AddCurve +Curve 00000007 1 2 00000001 00000005 +CCtrl 0 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000088818 -4.99999999999999911182 -10.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 0.00000000000000061232 -10.00000000000000000000 +CurvePt 0 -4.64894150531215188948 -1.84047354780936300855 -10.00000000000000000000 +CurvePt 0 -4.18497755609325672310 -2.73604876692571208352 -10.00000000000000000000 +CurvePt 0 -3.53553390593273864084 -3.53553390593273730858 -10.00000000000000000000 +CurvePt 0 -2.73604876692571297170 -4.18497755609325672310 -10.00000000000000000000 +CurvePt 0 -1.84047354780936478491 -4.64894150531215188948 -10.00000000000000000000 +CurvePt 1 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 +AddCurve +Curve 00000008 1 2 00000002 00000005 +CCtrl 0 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000088818 -4.99999999999999911182 0.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 0.00000000000000061232 0.00000000000000000000 +CurvePt 0 -4.64894150531215188948 -1.84047354780936300855 0.00000000000000000000 +CurvePt 0 -4.18497755609325672310 -2.73604876692571208352 0.00000000000000000000 +CurvePt 0 -3.53553390593273864084 -3.53553390593273730858 0.00000000000000000000 +CurvePt 0 -2.73604876692571297170 -4.18497755609325672310 0.00000000000000000000 +CurvePt 0 -1.84047354780936478491 -4.64894150531215188948 0.00000000000000000000 +CurvePt 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000009 1 1 00000005 00000006 +CCtrl 0 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 +CurvePt 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 0000000a 1 2 00000001 00000006 +CCtrl 0 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 4.99999999999999911182 -5.00000000000000088818 -10.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -0.00000000000000091849 -5.00000000000000000000 -10.00000000000000000000 +CurvePt 0 1.84047354780936300855 -4.64894150531215188948 -10.00000000000000000000 +CurvePt 0 2.73604876692571163943 -4.18497755609325672310 -10.00000000000000000000 +CurvePt 0 3.53553390593273730858 -3.53553390593273864084 -10.00000000000000000000 +CurvePt 0 4.18497755609325583492 -2.73604876692571297170 -10.00000000000000000000 +CurvePt 0 4.64894150531215188948 -1.84047354780936500696 -10.00000000000000000000 +CurvePt 1 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 +AddCurve +Curve 0000000b 1 2 00000002 00000006 +CCtrl 0 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 4.99999999999999911182 -5.00000000000000088818 0.00000000000000000000 Weight 0.70710678118654757274 +CCtrl 2 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -0.00000000000000091849 -5.00000000000000000000 0.00000000000000000000 +CurvePt 0 1.84047354780936300855 -4.64894150531215188948 0.00000000000000000000 +CurvePt 0 2.73604876692571163943 -4.18497755609325672310 0.00000000000000000000 +CurvePt 0 3.53553390593273730858 -3.53553390593273864084 0.00000000000000000000 +CurvePt 0 4.18497755609325583492 -2.73604876692571297170 0.00000000000000000000 +CurvePt 0 4.64894150531215188948 -1.84047354780936500696 0.00000000000000000000 +CurvePt 1 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 +AddCurve +Curve 0000000c 1 1 00000006 00000003 +CCtrl 0 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 -0.00000000000000122465 -10.00000000000000000000 +CurvePt 1 5.00000000000000000000 -0.00000000000000122465 0.00000000000000000000 +AddCurve diff --git a/test/constraint/pt_on_face/test.cpp b/test/constraint/pt_on_face/test.cpp new file mode 100644 index 0000000..5e9db1b --- /dev/null +++ b/test/constraint/pt_on_face/test.cpp @@ -0,0 +1,17 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} diff --git a/test/constraint/pt_on_line/free_in_3d_v20.slvs b/test/constraint/pt_on_line/free_in_3d_v20.slvs new file mode 100644 index 0000000..bc5ecec --- /dev/null +++ b/test/constraint/pt_on_line/free_in_3d_v20.slvs @@ -0,0 +1,315 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-20.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040015 +AddParam + +Param.h.v.=00050010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-20.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=42 +Constraint.group.v=00000002 +Constraint.ptA.v=00050000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/pt_on_line/free_in_3d_v22.slvs b/test/constraint/pt_on_line/free_in_3d_v22.slvs new file mode 100644 index 0000000..250f93c --- /dev/null +++ b/test/constraint/pt_on_line/free_in_3d_v22.slvs @@ -0,0 +1,315 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-20.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040015 +AddParam + +Param.h.v.=00050010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-20.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=42 +Constraint.group.v=00000002 +Constraint.ptA.v=00050000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/pt_on_line/left_free_in_3d.png b/test/constraint/pt_on_line/left_free_in_3d.png new file mode 100644 index 0000000000000000000000000000000000000000..13b2a6af4d1d36cefbff76aea8430f6bc7e0248e GIT binary patch literal 4319 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 HhW7mm%p2UJ literal 0 HcmV?d00001 diff --git a/test/constraint/pt_on_line/normal.slvs b/test/constraint/pt_on_line/normal.slvs new file mode 100644 index 0000000..6bd8985 --- /dev/null +++ b/test/constraint/pt_on_line/normal.slvs @@ -0,0 +1,318 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=40000001 +Param.val=0.33333333333333331483 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=42 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valP.v=40000001 +Constraint.ptA.v=00050000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/pt_on_line/normal_v20.slvs b/test/constraint/pt_on_line/normal_v20.slvs new file mode 100644 index 0000000..2c3eec5 --- /dev/null +++ b/test/constraint/pt_on_line/normal_v20.slvs @@ -0,0 +1,315 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=42 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/pt_on_line/normal_v22.slvs b/test/constraint/pt_on_line/normal_v22.slvs new file mode 100644 index 0000000..5a74c34 --- /dev/null +++ b/test/constraint/pt_on_line/normal_v22.slvs @@ -0,0 +1,313 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=42 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/pt_on_line/right_free_in_3d.png b/test/constraint/pt_on_line/right_free_in_3d.png new file mode 100644 index 0000000000000000000000000000000000000000..fca580fa96ba0cdef71493b8f3e7fdee37dd32ed GIT binary patch literal 4316 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 X-ZE6bB!|P_QNU)$Z4L$NiKqVuaAqb_ literal 0 HcmV?d00001 diff --git a/test/constraint/pt_pt_distance/free_in_3d.slvs b/test/constraint/pt_pt_distance/free_in_3d.slvs new file mode 100644 index 0000000..3da9edc --- /dev/null +++ b/test/constraint/pt_pt_distance/free_in_3d.slvs @@ -0,0 +1,289 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00050010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=30 +Constraint.group.v=00000002 +Constraint.valA=10.00000000000000000000 +Constraint.ptA.v=00040000 +Constraint.ptB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +Constraint.disp.offset.y=-1.49454882168925862196 +AddConstraint + diff --git a/test/constraint/pt_pt_distance/free_in_3d_v20.slvs b/test/constraint/pt_pt_distance/free_in_3d_v20.slvs new file mode 100644 index 0000000..e9b6d25 --- /dev/null +++ b/test/constraint/pt_pt_distance/free_in_3d_v20.slvs @@ -0,0 +1,289 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00050010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=30 +Constraint.group.v=00000002 +Constraint.valA=10.00000000000000000000 +Constraint.ptA.v=00040000 +Constraint.ptB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +Constraint.disp.offset.y=-1.49454882168925860000 +AddConstraint + diff --git a/test/constraint/pt_pt_distance/free_in_3d_v22.slvs b/test/constraint/pt_pt_distance/free_in_3d_v22.slvs new file mode 100644 index 0000000..c5b1af5 --- /dev/null +++ b/test/constraint/pt_pt_distance/free_in_3d_v22.slvs @@ -0,0 +1,289 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00050010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=30 +Constraint.group.v=00000002 +Constraint.valA=10.00000000000000000000 +Constraint.ptA.v=00040000 +Constraint.ptB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +Constraint.disp.offset.y=-1.49454882168925862196 +AddConstraint + diff --git a/test/constraint/pt_pt_distance/normal.png b/test/constraint/pt_pt_distance/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..b6524acec3e455fd5269d5b37649293ffc878ab9 GIT binary patch literal 4467 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 literal 0 HcmV?d00001 diff --git a/test/constraint/pt_pt_distance/reference.slvs b/test/constraint/pt_pt_distance/reference.slvs new file mode 100644 index 0000000..2cf72b5 --- /dev/null +++ b/test/constraint/pt_pt_distance/reference.slvs @@ -0,0 +1,289 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=30 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=10.00000000000000000000 +Constraint.ptA.v=00040000 +Constraint.ptB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=1 +Constraint.disp.offset.y=2.50000000000000000000 +AddConstraint + diff --git a/test/constraint/pt_pt_distance/reference_v20.slvs b/test/constraint/pt_pt_distance/reference_v20.slvs new file mode 100644 index 0000000..0888aa8 --- /dev/null +++ b/test/constraint/pt_pt_distance/reference_v20.slvs @@ -0,0 +1,289 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=30 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=10.00000000000000000000 +Constraint.ptA.v=00040000 +Constraint.ptB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=1 +Constraint.disp.offset.y=2.50000000000000000000 +AddConstraint + diff --git a/test/constraint/pt_pt_distance/reference_v22.slvs b/test/constraint/pt_pt_distance/reference_v22.slvs new file mode 100644 index 0000000..8a3b2a3 --- /dev/null +++ b/test/constraint/pt_pt_distance/reference_v22.slvs @@ -0,0 +1,289 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=30 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.valA=10.00000000000000000000 +Constraint.ptA.v=00040000 +Constraint.ptB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=1 +Constraint.disp.offset.y=2.50000000000000000000 +AddConstraint + diff --git a/test/constraint/pt_pt_distance/test.cpp b/test/constraint/pt_pt_distance/test.cpp new file mode 100644 index 0000000..97bef40 --- /dev/null +++ b/test/constraint/pt_pt_distance/test.cpp @@ -0,0 +1,49 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(free_in_3d_roundtrip) { + CHECK_LOAD("free_in_3d.slvs"); + CHECK_RENDER("free_in_3d.png"); + CHECK_SAVE("free_in_3d.slvs"); +} + +TEST_CASE(free_in_3d_migrate_from_v20) { + CHECK_LOAD("free_in_3d_v20.slvs"); + CHECK_SAVE("free_in_3d.slvs"); +} + +TEST_CASE(free_in_3d_migrate_from_v22) { + CHECK_LOAD("free_in_3d_v22.slvs"); + CHECK_SAVE("free_in_3d.slvs"); +} + +TEST_CASE(reference_roundtrip) { + CHECK_LOAD("reference.slvs"); + CHECK_RENDER("reference.png"); + CHECK_SAVE("reference.slvs"); +} + +TEST_CASE(reference_migrate_from_v20) { + CHECK_LOAD("reference_v20.slvs"); + CHECK_SAVE("reference.slvs"); +} + +TEST_CASE(reference_migrate_from_v22) { + CHECK_LOAD("reference_v22.slvs"); + CHECK_SAVE("reference.slvs"); +} diff --git a/test/constraint/same_orientation/normal.png b/test/constraint/same_orientation/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..7bcd4c59ea0425d118cf4ed2a1cd73861fdbc00a GIT binary patch literal 4472 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 b^jo<$rr0iEKi7l*Q32cBw!7wTCM5hDA_6I) literal 0 HcmV?d00001 diff --git a/test/constraint/same_orientation/normal.slvs b/test/constraint/same_orientation/normal.slvs new file mode 100644 index 0000000..fcadecd --- /dev/null +++ b/test/constraint/same_orientation/normal.slvs @@ -0,0 +1,309 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040020 +Param.val=0.70710678118654757274 +AddParam + +Param.h.v.=00040021 +Param.val=-0.70710678118654746172 +AddParam + +Param.h.v.=00040022 +AddParam + +Param.h.v.=00040023 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=40000002 +Param.val=1.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.actNormal.w=0.70710678118654757274 +Entity.actNormal.vx=-0.70710678118654746172 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000002 +Constraint.type=110 +Constraint.group.v=00000002 +Constraint.valP.v=40000002 +Constraint.entityA.v=00040020 +Constraint.entityB.v=00030020 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/same_orientation/normal_v20.slvs b/test/constraint/same_orientation/normal_v20.slvs new file mode 100644 index 0000000..99cbe1f --- /dev/null +++ b/test/constraint/same_orientation/normal_v20.slvs @@ -0,0 +1,302 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040020 +Param.val=0.70710678118654757000 +AddParam + +Param.h.v.=00040021 +Param.val=-0.70710678118654746000 +AddParam + +Param.h.v.=00040022 +AddParam + +Param.h.v.=00040023 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.actNormal.w=0.70710678118654757000 +Entity.actNormal.vx=-0.70710678118654746000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000002 +Constraint.type=110 +Constraint.group.v=00000002 +Constraint.entityA.v=00040020 +Constraint.entityB.v=00030020 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/same_orientation/normal_v22.slvs b/test/constraint/same_orientation/normal_v22.slvs new file mode 100644 index 0000000..c29b8d7 --- /dev/null +++ b/test/constraint/same_orientation/normal_v22.slvs @@ -0,0 +1,304 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040020 +Param.val=0.70710678118654757000 +AddParam + +Param.h.v.=00040021 +Param.val=-0.70710678118654746000 +AddParam + +Param.h.v.=00040022 +AddParam + +Param.h.v.=00040023 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.actNormal.w=0.70710678118654757000 +Entity.actNormal.vx=-0.70710678118654746000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000002 +Constraint.type=110 +Constraint.group.v=00000002 +Constraint.entityA.v=00040020 +Constraint.entityB.v=00030020 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/same_orientation/same_group.png b/test/constraint/same_orientation/same_group.png new file mode 100644 index 0000000000000000000000000000000000000000..ce3f77ba7366743776aa46ad410fb2377f579c11 GIT binary patch literal 4376 zcmeHL3p7;g9{)ThgG8KAbd*+f4>BT;lQX@P9Q1M~6Jd%t#duUi2E!4FLpSA>j_z`! zGbS_9%s3dfZYPRk@@UB9(Lqz5$zd=v=I#+KQ%UQrd(T>Tt-IFD>^@fW!S9;xuocm{wbk7@k7}hzLkvv=0F%mKV*qgG z(u!b>15OIW${PYADjES)^r!&blT`=XG9YPyRD*AQ^uH8D)xq1kw~+As{v1jef6T+` zCn2YD?j!nPz=%^iuHE8Y{9~C_K&v0!m610ZXCJ%hd>%CX;T;V@S@l{H^QPW(N1Mt4 zqKeu=D}^lCv~UE7N6rPXHL$yd0Q#rHY)Vj+JOgiui!Pio&1!lu1!y`ph3%9`Fa5hG z9ZV!3W@bnX&(HdUJB95Az|v+efLZ}<>XQR{$79}>hZwfm&Ezu+16k5a?zoFbri<;C_nYd6b_qsW& zFvC$?b)QY+>+jjqX?;7WLn1--&NQlEaM(kX!Z2c}RB*LK+$K@+3oi0TZFV%xUn9q} z$ht2*xf6eSV)p=DL=IamzfCJ0DfL#=T3geRqVlCB!{Hc=QKIv=RZ-%5&#$^>JOd@= z|Ky`U{k^Pw>FSN66Cl)jjszd*DQ=+_M_i+5vpUzjs9Hb*Pi7`bY&%aegrYVvd0G_>~oief*0iZ zeAnq$BnRWSm;vaUKSOeWqTqe4$nS_M7O+`@#|y|LkM=}RQomrT<{Mu`G_4RHzXX)% zs~Hk6<+Lk;y|9|c3P|ZSsxE=LGrF_wF=fi*dnuMZPU>0o33Y3WXhb$gCxD(ZO*mFL zEy%Bl;jW_+g9PRs1u32Go4#;)Z7r(~$z*s}ojy!D8DRD1Y|fKP{G`x>lP~tZ&M46^ zkLi!$-02pU_nMpWgL}||zKD=3LXN0-pz`^%$68pK>r12c0&;w5w~edD`grk_7q0I4 z&Xs84S8GT=TSQt5E`WV6edG;7^`sRwtH17O<}@p0ZFVzK!$Q9>nM*BOofI8zcZJCt z5>ZYD5XTJ2CK%ixjbdU!YLJ~u=z1|*kLb#NLLlIPDQRCf&N3wsxNo+kn(+@U0t?<( z)=xASFKBq9_!V=qJ`=dO9effdIBmU7==S?wx3Ecrj21dg`_P=x&IEpjLC;GJY({L_ z;9~%IUT@u*7?eSfVVwLr;9VE7i00zlIqXx|_mw}Krwf4hv02CeWJ3nG2QDr2oigBA zs{Grw?ZMGEO3z5I%Gb`x?jo%mF%0iodlVtj^UWwd02v9h*POTJkCgQPU=WMU{{|RZ zM-l7}Cfxjour#n3T}4n*z+DQryjxTys0g=UAB84soPLeM2sz<~04S3MU2uMvK89eI z(pRqnT|dn>3aDkN0bPgQ?{_3YO17Yu_pAaL`vY+JYcdR0er^BBGUBM zqFDH^XEH=Omq31);VlJaz?3LTX!<4--p59sG44X6?JxEAIVa&Lm2ny%@XOI7x`Z9M zwz{D1WTQM=zxXK* zs$%uM#9KA2z9+oxc70SJ+}AsHZEYdI9bxg&Sp5k7NuExMOnFdawGkA~(!#1mZZiXc z%iz74nkRCXk-z#o0^FCM+1SXFona^-mtzAqGq0loD*ZHq&6jVynb7EynwQJBoZ|7) ztf!rMS6y~;Bc2K)WQi8_E3me1rP;iOO8!7&XK9v8)gEj2=u{o13Wd#SkGDCYN zd*uLT^C=uNC2w8RE#86kNSxtxHL?WL6xOY+GcG`1K~I(0o1FP$QzvIlc+br$icPz1 ze;GZ8!5D07Il?c??I#QiOO4%%gpVRCOLKC099l%;Y2bf1$J@1qB_h14w0aZF7#MYD zaNAl4A+E`UcAx69vEhnSTUSp`uOH_>$+?0C-R`>Czxkw= z@>16}rBvN9+`$q(qmk{$&{y4WcH8rK`!5)WejfM}hf;TRge%z&qP~P=^v@?XUA+$A9S6V@hsMYj~IW|%K@R>M&D!G*W*@Qf!}xlyIp&23U}aQ{tfia`LzH5 literal 0 HcmV?d00001 diff --git a/test/constraint/same_orientation/same_group.slvs b/test/constraint/same_orientation/same_group.slvs new file mode 100644 index 0000000..7944b1e --- /dev/null +++ b/test/constraint/same_orientation/same_group.slvs @@ -0,0 +1,379 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-25.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040020 +Param.val=0.70710678118654757274 +AddParam + +Param.h.v.=00040021 +AddParam + +Param.h.v.=00040022 +Param.val=0.70710678118654746172 +AddParam + +Param.h.v.=00040023 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Param.h.v.=00050020 +Param.val=0.70710678118654757274 +AddParam + +Param.h.v.=00050021 +AddParam + +Param.h.v.=00050022 +Param.val=-0.70710678118654757274 +AddParam + +Param.h.v.=00050023 +AddParam + +Param.h.v.=00050040 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=40000002 +Param.val=-1.00000000000000022204 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=400 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-25.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.actNormal.w=0.70710678118654757274 +Entity.actNormal.vy=0.70710678118654746172 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.normal.v=00050020 +Entity.distance.v=00050040 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.actNormal.w=0.70710678118654757274 +Entity.actNormal.vy=-0.70710678118654757274 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050040 +Entity.type=4000 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000002 +Constraint.type=110 +Constraint.group.v=00000002 +Constraint.valP.v=40000002 +Constraint.entityA.v=00050020 +Constraint.entityB.v=00040020 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/same_orientation/test.cpp b/test/constraint/same_orientation/test.cpp new file mode 100644 index 0000000..9377b28 --- /dev/null +++ b/test/constraint/same_orientation/test.cpp @@ -0,0 +1,23 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(same_group_roundtrip) { + CHECK_LOAD("same_group.slvs"); + CHECK_RENDER("same_group.png"); + CHECK_SAVE("same_group.slvs"); +} diff --git a/test/constraint/symmetric/free_in_3d.png b/test/constraint/symmetric/free_in_3d.png new file mode 100644 index 0000000000000000000000000000000000000000..58c2ecc2fff5fa2427a8f6192802cb3b8bd5dc32 GIT binary patch literal 4378 zcmeHLX;f2J9=}NltLzv+6s#T8QG@`pjDVpOM2o0^WkT3uWwC;yZNnCp1ausz5-S}9 zMZ{7?g#a?fU^MV33aDifAwiS`ijru6AP|tyKpzNbTuzyDW+FZbmB@BjX` z_tJK3b5~bgq6z>|-?G`&8vrDJ`lGA}->D4Cehh$C_ZHXBcE;xRcfL6IsNS%)`}VQZ z2Tb<&umx~f~BH?*2*#feo?>x=j{K{q&|;_IfCnq(D7j| zEg<=K_r~x2wB~jPyVbNN|K(FnU)ZL zpY*xhZfqHRoV605Xz6}vf6lnx>+wq9wDQ~pUw!;08G!P)&Nc#D@{>c@0{bMaE8i&w zLeF>0;BcOM0&w(}!iO5(Op7bwA)uA>E487syl?=!dam-LMZ%1n1)hoYidmZji1+!f zQ%mlYJ?RWayeCCSV~qCw&5 z1=`6Y$|qtUou>ex&2vqD7j3d^!SiJxa>X1R@qt7`2@0fI%~lRoqz_~-5ViRx_q^Gt zd^@&Q7(9Z%XjJVJ3(oK-M#}?1?g^FG z@_GO%;zvj@R+Bei;UMbD!85yVJ#?|ew(Ay^BGCZp;==UjiPW@rWxdz? zS^*%#lAt^u#71;s6)?{Av{8rhLztzTXUftVDUji=QigT#zyaHk@~ti&x+1%|fhUzv zWiR>3>1OsORP;zs>15@^OKH}*=9FPrFfE9u&;l#Cs1w%7qeaq_rd5*#HuR`Ok+^EI zzhk(iqr5+Xe>P72c2Z=VCQh2SFEmkfHdK}p@%cU?={rk>^nhe~o7I#?$@ftf{D#7S z3xCHAB=s6im8l{1L4~~UiRwXP@wP5tZ2)yAd zR!^uR>->|e^H!u{W>1xf2y8UPCx;MGauZu7syLP8dDsDAuc>Qh(BA*#?qAfpY+&dN zSTU^F$k=66bSj~$WGemgGj2#adJqYtWyCiCF}MM~7~bO&nvO2RQPHRlE4Gdwn9#;! zkJFioof{R}PeLhFN8N2J)QC6|)ut)5P!Y6e&hkaNYe?O(NXYtR&J$iol;F#Dpzn#G zEqQ99m&*^jsZYs#+v2KKwobkHR*9NP$U5;rbkh=iY`Q7tJ@zG)W4IZ28hI2(936oH?TRw%p$eoICBRZ z0Oifbyk^bJs4l;^Q`l=3Pcx?2wk+YE4Z!AiKm^PWXt1bo5_1l&b+jC&3th9Ab2rEl zK*kx`Fmq7TU%O_)iD}9Xam+~Ef8ix8-;XZ4HX}D77RnkSpWF8AtE%sY!95*?HqHqXJ zRFaXW{{=6mm#K2T#UxF8S|V#LliG*(t!7Ke;@i!fM+w}|M(_HknxwIqqhou{h`6J| z-NrH=Cy~eM93wL$apeQ<+=tRiCxU-2YVc}M5|7z+xC=%ScBJNI%v0vI`3?E}sly*ih@8^MNtC0910xucl z?bOGIza*s7H7%TK$Oy2iWsFM<+>XhR)*lwt$44b|OS_%gv`2a4xQRS5W1v7%#6fiF z!SEBiwRjag^J5}N7FT)XhBk3ckbpl-+BY6XrC;VJ16GTeOJqw(mZf=Cj^vHWTTswn zM~v{|xX=)%bQZ1eX-;A5aJK&;pl4sZ?w9b+8^=bUS1Frxay%Sy&&nIaIjl~Ps0UZw zOJ($?8nXD&zjeeoV53^vRv2%7nERl+n@tMPm@>Ctnz1e8#eQwyOWAn{aAYU>`$4bc z31P)d)96t=j>K!abxK0V4N22Pwe@0}gdQcM(T(88;sP`K5-(#e0{VK!ErL_!!{dh@ i58vkafA1~Agwoo9h=Lbv>tXnv2iW4a&9%gZnDPgg(dEJb literal 0 HcmV?d00001 diff --git a/test/constraint/symmetric/free_in_3d.slvs b/test/constraint/symmetric/free_in_3d.slvs new file mode 100644 index 0000000..863a794 --- /dev/null +++ b/test/constraint/symmetric/free_in_3d.slvs @@ -0,0 +1,289 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Param.h.v.=00060010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00060012 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=101 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=60 +Constraint.group.v=00000002 +Constraint.ptA.v=00050000 +Constraint.ptB.v=00060000 +Constraint.entityA.v=00030000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/symmetric/free_in_3d_v20.slvs b/test/constraint/symmetric/free_in_3d_v20.slvs new file mode 100644 index 0000000..a6a0cea --- /dev/null +++ b/test/constraint/symmetric/free_in_3d_v20.slvs @@ -0,0 +1,287 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Param.h.v.=00060010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00060012 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=101 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=60 +Constraint.group.v=00000002 +Constraint.ptA.v=00050000 +Constraint.ptB.v=00060000 +Constraint.entityA.v=00030000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/symmetric/free_in_3d_v22.slvs b/test/constraint/symmetric/free_in_3d_v22.slvs new file mode 100644 index 0000000..ff95075 --- /dev/null +++ b/test/constraint/symmetric/free_in_3d_v22.slvs @@ -0,0 +1,289 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050012 +AddParam + +Param.h.v.=00060010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00060012 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=101 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=60 +Constraint.group.v=00000002 +Constraint.ptA.v=00050000 +Constraint.ptB.v=00060000 +Constraint.entityA.v=00030000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/symmetric/normal.png b/test/constraint/symmetric/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..d497e0f9c92c90575f962909c72011949f8a1c06 GIT binary patch literal 4362 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 literal 0 HcmV?d00001 diff --git a/test/constraint/symmetric_line/normal.slvs b/test/constraint/symmetric_line/normal.slvs new file mode 100644 index 0000000..22a436a --- /dev/null +++ b/test/constraint/symmetric_line/normal.slvs @@ -0,0 +1,338 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=63 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.ptB.v=00060000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/symmetric_line/normal_v20.slvs b/test/constraint/symmetric_line/normal_v20.slvs new file mode 100644 index 0000000..f1c4562 --- /dev/null +++ b/test/constraint/symmetric_line/normal_v20.slvs @@ -0,0 +1,336 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=63 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.ptB.v=00060000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/symmetric_line/normal_v22.slvs b/test/constraint/symmetric_line/normal_v22.slvs new file mode 100644 index 0000000..f0d4dca --- /dev/null +++ b/test/constraint/symmetric_line/normal_v22.slvs @@ -0,0 +1,338 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-15.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=-10.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-15.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=63 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050000 +Constraint.ptB.v=00060000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/symmetric_line/test.cpp b/test/constraint/symmetric_line/test.cpp new file mode 100644 index 0000000..5e9db1b --- /dev/null +++ b/test/constraint/symmetric_line/test.cpp @@ -0,0 +1,17 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} diff --git a/test/constraint/symmetric_vert/normal.png b/test/constraint/symmetric_vert/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..58c2ecc2fff5fa2427a8f6192802cb3b8bd5dc32 GIT binary patch literal 4378 zcmeHLX;f2J9=}NltLzv+6s#T8QG@`pjDVpOM2o0^WkT3uWwC;yZNnCp1ausz5-S}9 zMZ{7?g#a?fU^MV33aDifAwiS`ijru6AP|tyKpzNbTuzyDW+FZbmB@BjX` z_tJK3b5~bgq6z>|-?G`&8vrDJ`lGA}->D4Cehh$C_ZHXBcE;xRcfL6IsNS%)`}VQZ z2Tb<&umx~f~BH?*2*#feo?>x=j{K{q&|;_IfCnq(D7j| zEg<=K_r~x2wB~jPyVbNN|K(FnU)ZL zpY*xhZfqHRoV605Xz6}vf6lnx>+wq9wDQ~pUw!;08G!P)&Nc#D@{>c@0{bMaE8i&w zLeF>0;BcOM0&w(}!iO5(Op7bwA)uA>E487syl?=!dam-LMZ%1n1)hoYidmZji1+!f zQ%mlYJ?RWayeCCSV~qCw&5 z1=`6Y$|qtUou>ex&2vqD7j3d^!SiJxa>X1R@qt7`2@0fI%~lRoqz_~-5ViRx_q^Gt zd^@&Q7(9Z%XjJVJ3(oK-M#}?1?g^FG z@_GO%;zvj@R+Bei;UMbD!85yVJ#?|ew(Ay^BGCZp;==UjiPW@rWxdz? zS^*%#lAt^u#71;s6)?{Av{8rhLztzTXUftVDUji=QigT#zyaHk@~ti&x+1%|fhUzv zWiR>3>1OsORP;zs>15@^OKH}*=9FPrFfE9u&;l#Cs1w%7qeaq_rd5*#HuR`Ok+^EI zzhk(iqr5+Xe>P72c2Z=VCQh2SFEmkfHdK}p@%cU?={rk>^nhe~o7I#?$@ftf{D#7S z3xCHAB=s6im8l{1L4~~UiRwXP@wP5tZ2)yAd zR!^uR>->|e^H!u{W>1xf2y8UPCx;MGauZu7syLP8dDsDAuc>Qh(BA*#?qAfpY+&dN zSTU^F$k=66bSj~$WGemgGj2#adJqYtWyCiCF}MM~7~bO&nvO2RQPHRlE4Gdwn9#;! zkJFioof{R}PeLhFN8N2J)QC6|)ut)5P!Y6e&hkaNYe?O(NXYtR&J$iol;F#Dpzn#G zEqQ99m&*^jsZYs#+v2KKwobkHR*9NP$U5;rbkh=iY`Q7tJ@zG)W4IZ28hI2(936oH?TRw%p$eoICBRZ z0Oifbyk^bJs4l;^Q`l=3Pcx?2wk+YE4Z!AiKm^PWXt1bo5_1l&b+jC&3th9Ab2rEl zK*kx`Fmq7TU%O_)iD}9Xam+~Ef8ix8-;XZ4HX}D77RnkSpWF8AtE%sY!95*?HqHqXJ zRFaXW{{=6mm#K2T#UxF8S|V#LliG*(t!7Ke;@i!fM+w}|M(_HknxwIqqhou{h`6J| z-NrH=Cy~eM93wL$apeQ<+=tRiCxU-2YVc}M5|7z+xC=%ScBJNI%v0vI`3?E}sly*ih@8^MNtC0910xucl z?bOGIza*s7H7%TK$Oy2iWsFM<+>XhR)*lwt$44b|OS_%gv`2a4xQRS5W1v7%#6fiF z!SEBiwRjag^J5}N7FT)XhBk3ckbpl-+BY6XrC;VJ16GTeOJqw(mZf=Cj^vHWTTswn zM~v{|xX=)%bQZ1eX-;A5aJK&;pl4sZ?w9b+8^=bUS1Frxay%Sy&&nIaIjl~Ps0UZw zOJ($?8nXD&zjeeoV53^vRv2%7nERl+n@tMPm@>Ctnz1e8#eQwyOWAn{aAYU>`$4bc z31P)d)96t=j>K!abxK0V4N22Pwe@0}gdQcM(T(88;sP`K5-(#e0{VK!ErL_!!{dh@ i58vkafA1~Agwoo9h=Lbv>tXnv2iW4a&9%gZnDPgg(dEJb literal 0 HcmV?d00001 diff --git a/test/constraint/symmetric_vert/normal.slvs b/test/constraint/symmetric_vert/normal.slvs new file mode 100644 index 0000000..dc23f9e --- /dev/null +++ b/test/constraint/symmetric_vert/normal.slvs @@ -0,0 +1,287 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=-5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=62 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040000 +Constraint.ptB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/symmetric_vert/normal_v20.slvs b/test/constraint/symmetric_vert/normal_v20.slvs new file mode 100644 index 0000000..c6f5a6d --- /dev/null +++ b/test/constraint/symmetric_vert/normal_v20.slvs @@ -0,0 +1,285 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=-5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=62 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040000 +Constraint.ptB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/symmetric_vert/normal_v22.slvs b/test/constraint/symmetric_vert/normal_v22.slvs new file mode 100644 index 0000000..602bee1 --- /dev/null +++ b/test/constraint/symmetric_vert/normal_v22.slvs @@ -0,0 +1,287 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=-5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=62 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040000 +Constraint.ptB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/symmetric_vert/test.cpp b/test/constraint/symmetric_vert/test.cpp new file mode 100644 index 0000000..5e9db1b --- /dev/null +++ b/test/constraint/symmetric_vert/test.cpp @@ -0,0 +1,17 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} diff --git a/test/constraint/vertical/line.png b/test/constraint/vertical/line.png new file mode 100644 index 0000000000000000000000000000000000000000..096ca416be9b9c37379affb409e5e33f63df89b4 GIT binary patch literal 4331 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 literal 0 HcmV?d00001 diff --git a/test/constraint/vertical/pt_pt.slvs b/test/constraint/vertical/pt_pt.slvs new file mode 100644 index 0000000..0381c60 --- /dev/null +++ b/test/constraint/vertical/pt_pt.slvs @@ -0,0 +1,287 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=-5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=81 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040000 +Constraint.ptB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/vertical/pt_pt_v20.slvs b/test/constraint/vertical/pt_pt_v20.slvs new file mode 100644 index 0000000..648dbca --- /dev/null +++ b/test/constraint/vertical/pt_pt_v20.slvs @@ -0,0 +1,285 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=-5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=81 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040000 +Constraint.ptB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/vertical/pt_pt_v22.slvs b/test/constraint/vertical/pt_pt_v22.slvs new file mode 100644 index 0000000..458c241 --- /dev/null +++ b/test/constraint/vertical/pt_pt_v22.slvs @@ -0,0 +1,287 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=-5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=81 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040000 +Constraint.ptB.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/constraint/vertical/test.cpp b/test/constraint/vertical/test.cpp new file mode 100644 index 0000000..c08b5e1 --- /dev/null +++ b/test/constraint/vertical/test.cpp @@ -0,0 +1,33 @@ +#include "harness.h" + +TEST_CASE(line_roundtrip) { + CHECK_LOAD("line.slvs"); + CHECK_RENDER("line.png"); + CHECK_SAVE("line.slvs"); +} + +TEST_CASE(line_migrate_from_v20) { + CHECK_LOAD("line_v20.slvs"); + CHECK_SAVE("line.slvs"); +} + +TEST_CASE(line_migrate_from_v22) { + CHECK_LOAD("line_v22.slvs"); + CHECK_SAVE("line.slvs"); +} + +TEST_CASE(pt_pt_roundtrip) { + CHECK_LOAD("pt_pt.slvs"); + CHECK_RENDER("pt_pt.png"); + CHECK_SAVE("pt_pt.slvs"); +} + +TEST_CASE(pt_pt_migrate_from_v20) { + CHECK_LOAD("pt_pt_v20.slvs"); + CHECK_SAVE("pt_pt.slvs"); +} + +TEST_CASE(pt_pt_migrate_from_v22) { + CHECK_LOAD("pt_pt_v22.slvs"); + CHECK_SAVE("pt_pt.slvs"); +} diff --git a/test/constraint/where_dragged/free_in_3d.png b/test/constraint/where_dragged/free_in_3d.png new file mode 100644 index 0000000000000000000000000000000000000000..7ee4884d2052ce8d658088ff381281a3e7c51422 GIT binary patch literal 4257 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, 3.1415926); +} + +TEST_CASE(literal) { + Expr *e; + CHECK_PARSE(e, "42"); + CHECK_TRUE(e->Eval() == 42); + CHECK_PARSE(e, "42.5"); + CHECK_TRUE(e->Eval() == 42.5); + CHECK_PARSE(e, "1_000_000"); + CHECK_TRUE(e->Eval() == 1000000); +} + +TEST_CASE(unary_ops) { + Expr *e; + CHECK_PARSE(e, "-10"); + CHECK_TRUE(e->Eval() == -10); +} + +TEST_CASE(binary_ops) { + Expr *e; + CHECK_PARSE(e, "1 + 2"); + CHECK_TRUE(e->Eval() == 3); + CHECK_PARSE(e, "1 - 2"); + CHECK_TRUE(e->Eval() == -1); + CHECK_PARSE(e, "3 * 4"); + CHECK_TRUE(e->Eval() == 12); + CHECK_PARSE(e, "3 / 4"); + CHECK_TRUE(e->Eval() == 0.75); +} + +TEST_CASE(parentheses) { + Expr *e; + CHECK_PARSE(e, "(1 + 2) * 3"); + CHECK_TRUE(e->Eval() == 9); + CHECK_PARSE(e, "1 + (2 * 3)"); + CHECK_TRUE(e->Eval() == 7); +} + +TEST_CASE(functions) { + Expr *e; + CHECK_PARSE(e, "sqrt(2)"); + CHECK_EQ_EPS(e->Eval(), 1.414213); + CHECK_PARSE(e, "square(3)"); + CHECK_EQ_EPS(e->Eval(), 9); + CHECK_PARSE(e, "sin(180)"); + CHECK_EQ_EPS(e->Eval(), 0); + CHECK_PARSE(e, "sin(90)"); + CHECK_EQ_EPS(e->Eval(), 1); + CHECK_PARSE(e, "cos(180)"); + CHECK_EQ_EPS(e->Eval(), -1); + CHECK_PARSE(e, "asin(1)"); + CHECK_EQ_EPS(e->Eval(), 90); + CHECK_PARSE(e, "acos(0)"); + CHECK_EQ_EPS(e->Eval(), 90); +} + +TEST_CASE(variable) { + Expr *e; + CHECK_PARSE(e, "Var"); + CHECK_TRUE(e->op == Expr::Op::VARIABLE); +} + +TEST_CASE(precedence) { + Expr *e; + CHECK_PARSE(e, "2 + 3 * 4"); + CHECK_TRUE(e->Eval() == 14); + CHECK_PARSE(e, "2 - 3 / 4"); + CHECK_TRUE(e->Eval() == 1.25); + CHECK_PARSE(e, "-3 + 2"); + CHECK_TRUE(e->Eval() == -1); + CHECK_PARSE(e, "2 + 3 - 4"); + CHECK_TRUE(e->Eval() == 1); +} + +TEST_CASE(errors) { + CHECK_PARSE_ERR("\x01", + "Unexpected character"); + CHECK_PARSE_ERR("notavar", + "'notavar' is not a valid variable, function or constant"); + CHECK_PARSE_ERR("1e2e3", + "'1e2e3' is not a valid number"); + CHECK_PARSE_ERR("_", + "'_' is not a valid operator"); + CHECK_PARSE_ERR("2 2", + "Expected an operator"); + CHECK_PARSE_ERR("2 + +", + "Expected an operand"); + CHECK_PARSE_ERR("( 2 + 2", + "Expected ')'"); + CHECK_PARSE_ERR("(", + "Expected ')'"); +} diff --git a/test/core/locale/test.cpp b/test/core/locale/test.cpp new file mode 100644 index 0000000..2b0a92b --- /dev/null +++ b/test/core/locale/test.cpp @@ -0,0 +1,10 @@ +#include "ui.h" + +#include "harness.h" + +TEST_CASE(parseable) { + for(auto locale : Locales()) { + SetLocale(locale.Culture()); + } + CHECK_TRUE(true); // didn't crash +} diff --git a/test/core/path/test.cpp b/test/core/path/test.cpp new file mode 100644 index 0000000..90ff124 --- /dev/null +++ b/test/core/path/test.cpp @@ -0,0 +1,248 @@ +#include "harness.h" + +using Platform::Path; + +#if defined(WIN32) +#define S "\\" +#define R "C:" +#define U "\\\\?\\C:" +#else +#define S "/" +#define R "" +#define U "" +#endif + +TEST_CASE(from_raw) { + Path path = Path::From("/foo"); + CHECK_EQ_STR(path.raw, "/foo"); +} + +#if defined(WIN32) || defined(__APPLE__) +TEST_CASE(equals_win32_apple) { + CHECK_TRUE(Path::From(R S "foo").Equals(Path::From(R S "foo"))); + CHECK_TRUE(Path::From(R S "foo").Equals(Path::From(R S "FOO"))); + CHECK_FALSE(Path::From(R S "foo").Equals(Path::From(R S "bar"))); +} +#else +TEST_CASE(equals_unix) { + CHECK_TRUE(Path::From(R S "foo").Equals(Path::From(R S "foo"))); + CHECK_FALSE(Path::From(R S "foo").Equals(Path::From(R S "FOO"))); + CHECK_FALSE(Path::From(R S "foo").Equals(Path::From(R S "bar"))); +} +#endif + +#if defined(WIN32) +TEST_CASE(is_absolute_win32) { + CHECK_TRUE(Path::From("c:\\foo").IsAbsolute()); + CHECK_TRUE(Path::From("\\\\?\\c:\\").IsAbsolute()); + CHECK_TRUE(Path::From("\\\\server\\share\\").IsAbsolute()); + CHECK_FALSE(Path::From("c:/foo").IsAbsolute()); + CHECK_FALSE(Path::From("c:foo").IsAbsolute()); + CHECK_FALSE(Path::From("\\\\?").IsAbsolute()); + CHECK_FALSE(Path::From("\\\\server\\").IsAbsolute()); + CHECK_FALSE(Path::From("\\\\?\\c:").IsAbsolute()); + CHECK_FALSE(Path::From("\\\\server\\share").IsAbsolute()); + CHECK_FALSE(Path::From("foo").IsAbsolute()); + CHECK_FALSE(Path::From("/foo").IsAbsolute()); +} +#else +TEST_CASE(is_absolute_unix) { + CHECK_TRUE(Path::From("/foo").IsAbsolute()); + CHECK_FALSE(Path::From("c:/foo").IsAbsolute()); + CHECK_FALSE(Path::From("c:\\foo").IsAbsolute()); + CHECK_FALSE(Path::From("\\\\?\\foo").IsAbsolute()); + CHECK_FALSE(Path::From("c:foo").IsAbsolute()); + CHECK_FALSE(Path::From("foo").IsAbsolute()); +} +#endif + +TEST_CASE(has_extension) { + CHECK_TRUE(Path::From("foo.bar").HasExtension("bar")); + CHECK_TRUE(Path::From("foo.bar").HasExtension("BAR")); + CHECK_TRUE(Path::From("foo.bAr").HasExtension("BaR")); + CHECK_TRUE(Path::From("foo.bar").HasExtension("bar")); + CHECK_FALSE(Path::From("foo.bar").HasExtension("baz")); +} + +TEST_CASE(file_name) { + CHECK_EQ_STR(Path::From("foo").FileName(), "foo"); + CHECK_EQ_STR(Path::From("foo" S "bar").FileName(), "bar"); +} + +TEST_CASE(file_stem) { + CHECK_EQ_STR(Path::From("foo").FileStem(), "foo"); + CHECK_EQ_STR(Path::From("foo" S "bar").FileStem(), "bar"); + CHECK_EQ_STR(Path::From("foo.ext").FileStem(), "foo"); + CHECK_EQ_STR(Path::From("foo" S "bar.ext").FileStem(), "bar"); +} + +TEST_CASE(extension) { + CHECK_EQ_STR(Path::From("foo").Extension(), ""); + CHECK_EQ_STR(Path::From("foo.bar").Extension(), "bar"); + CHECK_EQ_STR(Path::From("foo.bar.baz").Extension(), "baz"); +} + +TEST_CASE(with_extension) { + CHECK_EQ_STR(Path::From("foo.bar").WithExtension("").raw, "foo"); + CHECK_EQ_STR(Path::From("foo.bar").WithExtension("baz").raw, "foo.baz"); + CHECK_EQ_STR(Path::From("foo").WithExtension("baz").raw, "foo.baz"); +} + +TEST_CASE(parent) { + Path path; + path = Path::From("foo" S "bar"); + CHECK_EQ_STR(path.Parent().raw, "foo" S); + path = Path::From("foo" S "bar" S); + CHECK_EQ_STR(path.Parent().raw, "foo" S); + path = Path::From(R S "foo" S "bar"); + CHECK_EQ_STR(path.Parent().raw, R S "foo" S); + path = Path::From(R S "foo"); + CHECK_EQ_STR(path.Parent().raw, R S); + + path = Path::From(""); + CHECK_TRUE(path.Parent().IsEmpty()); + path = Path::From("foo"); + CHECK_TRUE(path.Parent().IsEmpty()); + path = Path::From("foo" S); + CHECK_TRUE(path.Parent().IsEmpty()); + path = Path::From(R S); + CHECK_TRUE(path.Parent().IsEmpty()); +} + +#if defined(WIN32) +TEST_CASE(parent_win32) { + Path path; + path = Path::From(U S); + CHECK_TRUE(path.Parent().IsEmpty()); +} +#endif + +TEST_CASE(join) { + Path path; + path = Path::From("foo"); + CHECK_EQ_STR(path.Join(Path::From("bar")).raw, "foo" S "bar"); + path = Path::From("foo" S); + CHECK_EQ_STR(path.Join(Path::From("bar")).raw, "foo" S "bar"); + + path = Path::From(""); + CHECK_EQ_STR(path.Join(Path::From("bar")).raw, "." S "bar"); + path = Path::From("foo"); + CHECK_EQ_STR(path.Join(Path::From("")).raw, "foo" S); + path = Path::From("foo"); + CHECK_TRUE(path.Join(Path::From(R S "bar")).IsEmpty()); + path = Path::From(""); + CHECK_EQ_STR(path.Join(Path::From("")).raw, "." S); +} + +TEST_CASE(expand) { + Path path; + path = Path::From("foo"); + CHECK_EQ_STR(path.Expand().raw, "foo"); + path = Path::From("foo" S "bar"); + CHECK_EQ_STR(path.Expand().raw, "foo" S "bar"); + path = Path::From("foo" S); + CHECK_EQ_STR(path.Expand().raw, "foo"); + path = Path::From("foo" S "."); + CHECK_EQ_STR(path.Expand().raw, "foo"); + path = Path::From("foo" S "." S "bar"); + CHECK_EQ_STR(path.Expand().raw, "foo" S "bar"); + path = Path::From("foo" S ".." S "bar"); + CHECK_EQ_STR(path.Expand().raw, "bar"); + path = Path::From("foo" S ".."); + CHECK_EQ_STR(path.Expand().raw, "."); + path = Path::From(R S "foo" S ".."); + CHECK_EQ_STR(path.Expand().raw, U S); + path = Path::From(R S); + CHECK_EQ_STR(path.Expand().raw, U S); + + path = Path::From(R S ".."); + CHECK_TRUE(path.Expand().IsEmpty()); + path = Path::From(R S ".." S "foo"); + CHECK_TRUE(path.Expand().IsEmpty()); + path = Path::From(".."); + CHECK_TRUE(path.Expand().IsEmpty()); +} + +#if defined(WIN32) +TEST_CASE(expand_win32) { + Path path; + path = Path::From(R S "foo"); + CHECK_EQ_STR(path.Expand().raw, U S "foo"); + path = Path::From(U S "foo"); + CHECK_EQ_STR(path.Expand().raw, U S "foo"); +} +#endif + +TEST_CASE(expand_from_cwd) { + Path cwd = Path::CurrentDirectory().Expand(); + + Path path; + path = Path::From(R S "foo"); + CHECK_EQ_STR(path.Expand(/*fromCurrentDirectory=*/true).raw, + U S "foo"); + path = Path::From("foo" S "bar"); + CHECK_EQ_STR(path.Expand(/*fromCurrentDirectory=*/true).raw, + cwd.raw + S "foo" S "bar"); + path = Path::From(".." S "bar"); + CHECK_EQ_STR(path.Expand(/*fromCurrentDirectory=*/true).raw, + cwd.Parent().raw + "bar"); +} + +TEST_CASE(relative_to) { + Path base; + base = Path::From(R S "foo" S "bar"); + CHECK_EQ_STR(Path::From(R S "foo").RelativeTo(base).raw, + ".."); + base = Path::From(R S "foo" S "bar"); + CHECK_EQ_STR(Path::From(R S "foo" S "baz").RelativeTo(base).raw, + ".." S "baz"); + base = Path::From(R S "foo" S "bar"); + CHECK_EQ_STR(Path::From(R S "foo" S "bar" S "quux").RelativeTo(base).raw, + "quux"); + base = Path::From(R S "foo" S "bar"); + CHECK_EQ_STR(Path::From(R S "foo" S "bar").RelativeTo(base).raw, + "."); + + base = Path::From("foo"); + CHECK_TRUE(Path::From(R S "foo" S "bar").RelativeTo(base).IsEmpty()); + base = Path::From(R S "foo" S "bar"); + CHECK_TRUE(Path::From("foo").RelativeTo(base).IsEmpty()); +} + +#if defined(WIN32) +TEST_CASE(relative_to_win32) { + Path base; + base = Path::From("C:\\foo"); + CHECK_EQ_STR(Path::From("\\\\?\\C:\\bar").RelativeTo(base).raw, + "..\\bar"); + base = Path::From("C:\\foo"); + CHECK_EQ_STR(Path::From("c:\\FOO").RelativeTo(base).raw, + "."); + + base = Path::From("C:\\foo"); + CHECK_TRUE(Path::From("D:\\bar").RelativeTo(base).IsEmpty()); + CHECK_TRUE(Path::From("\\\\server\\share\\bar").RelativeTo(base).IsEmpty()); +} +#elif defined(__APPLE__) +TEST_CASE(relative_to_apple) { + Path base; + base = Path::From("/users/foo"); + CHECK_EQ_STR(Path::From("/Users/FOO").RelativeTo(base).raw, + "."); +} +#else +TEST_CASE(relative_to_unix) { + Path base; + base = Path::From("/users/foo"); + CHECK_EQ_STR(Path::From("/Users/FOO").RelativeTo(base).raw, + "../../Users/FOO"); +} +#endif + +TEST_CASE(from_portable) { + CHECK_EQ_STR(Path::FromPortable("foo/bar").raw, "foo" S "bar"); +} + +TEST_CASE(to_portable) { + CHECK_EQ_STR(Path::From("foo" S "bar").ToPortable(), "foo/bar"); +} diff --git a/test/debugtool.cpp b/test/debugtool.cpp new file mode 100644 index 0000000..aea6b84 --- /dev/null +++ b/test/debugtool.cpp @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +// Our entry point for exposing various internal mechanisms. +// +// Copyright 2017 whitequark +//----------------------------------------------------------------------------- + +#include "expr.h" +#include "platform/platform.h" + +using namespace SolveSpace; + +int main(int argc, char **argv) { + std::vector args = Platform::InitCli(argc, argv); + + if(args.size() == 3 && args[1] == "expr") { + std::string expr = args[2], err; + Expr *e = Expr::Parse(expr.c_str(), &err); + if(e == NULL) { + fprintf(stderr, "cannot parse: %s\n", err.c_str()); + } else { + fprintf(stderr, "%g\n", e->Eval()); + } + Platform::FreeAllTemporary(); + } else { + fprintf(stderr, "Usage: %s \n", args[0].c_str()); +//-----------------------------------------------------------------------------> 80 col */ + fprintf(stderr, R"( +Commands: + expr [expr] + Evaluate an expression. +)"); + } + + return 0; +} diff --git a/test/group/link/normal.png b/test/group/link/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..b3295bcfa81f43831cef8314582506fec98a01e0 GIT binary patch literal 4151 zcmeHLdr(t%7XIZn2?RDRf`E%!kWthL@+dM?il{t_7UZD>vY?PIiU{&hKpp`JYGrt6 z+FCI@1gljF2FSt&!Xx3bTBrglQUXXIp(!AQcjRS&>Y5Rt^Bv zc2#3QHA}_-4I?H1|3d44-E#O5OsL@1 zAv@hIbMz&yw-ONG(ls~xcjH_bL+rlQ8-C4Atj=(Lm<0isAJIw@q{hW!btXLaH{umn zHEW0lsuuMtt*tpAV@w+ya!|TlX36cG0L9uQCif>N1NerEva-<|xiO`HzQ66Hy+XVS zVtXq`6;Q9x6)yk2^aJLf&Z+?uLuFwB$-Aw^Tmcm(7oLK51G0yaGQgSx1U25E`SuWd zgF?LSqTy$LXaIkvH2Y=HC2MX{FhF;evao=0XJ}xJLU~F4g zU|SSlLm^%$F*?c=0_06fvzK#{8kf<^fvD-qY07Jlip)@;E5np&W2ON}-{6!nS<*R+ zfLwmuI!rNMMUi#2E($O;m1ZyB4X{naGd~pUC?`Q@!59YczX%UscsiIqU2(t`iEh8Iv1YxPQm{Qp5ZjmZqz3aE}*Z)P|>RbaYZ zlLQ#Y1xXH>k8dQy5c#ll$t|cJkBANLSoRMWk+aF{=zZ}m+M|)l^$rd?RuhF36mSW4 znaez8C|=5FotjBNh{Mm%PwERxT5PY?@H%=H1B;g|hT@KDGHFHoQtS-VLlDZV;a9!MPVK$|mCnXmzS|a_%lCRoffc zm~8%u0#tr*bxwwKBNLEv3bpYzw^7vSR6;hgi6s+PH|qC&V?Ye8Q$fHYU1s0->Nh?j z64sLr*t(6AqAzPgeI=iLKeZ@~SBmLNd}0&2wWybsOYi@7%L)C~HPY9t^ z=XKMU{@R%DglJ)!G~m<^tYxlLVb!m<&Ll>9M5heuR@u{+cN9(s-bO!gEqz?YJMb(< zt~(d36?CW5>%)^O%|AO6cOpU}Ok7b%R`fQp-p!+xf0Wc<>&t4uYDR0BFY@)dyX?G9L>*cO)}{y6 zCn6$Ll}MS29};O%=H}hL!ps(!GP571LR!)QuBf5y-~~JNu@t0Yc=^*^MZ-Z|l5tn& z%+9*S*^1-Rig2k_=TbR(e#V_8i%IggE$h{aXa6GWyX7w`{m>1u-|?eTd}DeqoV@^W ze7G&7c*esrZ=IE`Ai=V(-+aA(?}D*GQK6M>PXZ0ZJafho*qb4BdLuo2I5Zg3lKn6f zXDsCBm-wy<@2c=>7*N86(@DulKL-Hmus=E4&v`=`pc1O< z5Fv-Jyf#5FOA^DoihuJ;xh9;rL!pceh`ei9B@iIJGysJM-aL!EY|+eeZi_A`{0eCa z4>_ZN+}M|GR&ku6MvqdO!Y~Tm;<9nG3mZ#MhaAQLrrC;J;p;O}^hZuT-8ZLEW<~M$s{lv10v^t;!wr0 zggq58y8^IIi=Wg2sw5&U;_u~7hJX>=isP*j2F*>8OxJ<=ZV_;c zRZ%5tDj1>SSlC$wiRZUW8 zVbh60ZTh|3T&(C@SF-SBaizl;$rDH<_=cooH)n66cyGo#E8h+P*+~-)2gh1`vMt z$^izrSn&|cv*Zp0Tz_7dJngeF)d-O7B!1_nZMePh*S7!|-?Eley6SIWclnO@#H@oU zuA3p?X347e+LhN4K`AMPY#*F_4@p`MwwCxBnKKx3<^U_PyYkf${zMlUUeZq?FaC3< hV#Pso1xS{#P~G0xs?P;`kv{~$K1WxF51>gDwC7 literal 0 HcmV?d00001 diff --git a/test/group/link/normal.slvs b/test/group/link/normal.slvs new file mode 100644 index 0000000..97ba795 --- /dev/null +++ b/test/group/link/normal.slvs @@ -0,0 +1,465 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000003 +Group.type=5300 +Group.order=2 +Group.name=rect-v20 +Group.activeWorkplane.v=00010000 +Group.color=00646464 +Group.skipFirst=0 +Group.meshCombine=2 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ + 1 00010000 0 + 2 00010001 0 + 3 00010020 0 + 4 00020000 0 + 5 00020001 0 + 6 00020020 0 + 7 00030000 0 + 8 00030001 0 + 9 00030020 0 + 10 00040000 0 + 11 00040001 0 + 12 00040002 0 + 13 00050000 0 + 14 00050001 0 + 15 00050002 0 + 16 00060000 0 + 17 00060001 0 + 18 00060002 0 + 19 00070000 0 + 20 00070001 0 + 21 00070002 0 + 22 80020000 0 + 23 80020001 0 + 24 80020002 0 +} +Group.impFile=/TESTROOT/rect_v20.slvs +Group.impFileRel=rect_v20.slvs +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=80030000 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=80030001 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=80030002 +AddParam + +Param.h.v.=80030003 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=80030004 +AddParam + +Param.h.v.=80030005 +AddParam + +Param.h.v.=80030006 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=0 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030002 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030003 +Entity.type=3011 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030005 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030006 +Entity.type=3011 +Entity.construction=0 +Entity.point[0].v=80030005 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030008 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030009 +Entity.type=3011 +Entity.construction=0 +Entity.point[0].v=80030008 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000a +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000b +Entity.point[1].v=8003000c +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000b +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000c +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000d +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000e +Entity.point[1].v=8003000f +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000e +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000f +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030010 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030011 +Entity.point[1].v=80030012 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030011 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030012 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030013 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030014 +Entity.point[1].v=80030015 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030014 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030015 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030017 +Entity.type=3011 +Entity.construction=0 +Entity.point[0].v=80030018 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030018 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + diff --git a/test/group/link/normal_v20.slvs b/test/group/link/normal_v20.slvs new file mode 100644 index 0000000..f06c81d --- /dev/null +++ b/test/group/link/normal_v20.slvs @@ -0,0 +1,463 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000003 +Group.type=5300 +Group.order=2 +Group.name=rect-v20 +Group.activeWorkplane.v=00010000 +Group.opA.v=00000002 +Group.color=00646464 +Group.skipFirst=0 +Group.meshCombine=2 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ + 1 00010000 0 + 2 00010001 0 + 3 00010020 0 + 4 00020000 0 + 5 00020001 0 + 6 00020020 0 + 7 00030000 0 + 8 00030001 0 + 9 00030020 0 + 10 00040000 0 + 11 00040001 0 + 12 00040002 0 + 13 00050000 0 + 14 00050001 0 + 15 00050002 0 + 16 00060000 0 + 17 00060001 0 + 18 00060002 0 + 19 00070000 0 + 20 00070001 0 + 21 00070002 0 + 22 80020000 0 + 23 80020001 0 + 24 80020002 0 +} +Group.impFile=Z:\TESTROOT\rect_v20.slvs +Group.impFileRel=rect_v20.slvs +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=80030000 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=80030001 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=80030002 +AddParam + +Param.h.v.=80030003 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=80030004 +AddParam + +Param.h.v.=80030005 +AddParam + +Param.h.v.=80030006 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=0 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030002 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030003 +Entity.type=3011 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030005 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030006 +Entity.type=3011 +Entity.construction=0 +Entity.point[0].v=80030005 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030008 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030009 +Entity.type=3011 +Entity.construction=0 +Entity.point[0].v=80030008 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000a +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000b +Entity.point[1].v=8003000c +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000b +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000c +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000d +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000e +Entity.point[1].v=8003000f +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000e +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000f +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030010 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030011 +Entity.point[1].v=80030012 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030011 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030012 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030013 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030014 +Entity.point[1].v=80030015 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030014 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030015 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030017 +Entity.type=3011 +Entity.construction=0 +Entity.point[0].v=80030018 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030018 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + diff --git a/test/group/link/normal_v22.slvs b/test/group/link/normal_v22.slvs new file mode 100644 index 0000000..fb39ea2 --- /dev/null +++ b/test/group/link/normal_v22.slvs @@ -0,0 +1,465 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000003 +Group.type=5300 +Group.order=2 +Group.name=rect-v20 +Group.activeWorkplane.v=00010000 +Group.color=00646464 +Group.skipFirst=0 +Group.meshCombine=2 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ + 1 00010000 0 + 2 00010001 0 + 3 00010020 0 + 4 00020000 0 + 5 00020001 0 + 6 00020020 0 + 7 00030000 0 + 8 00030001 0 + 9 00030020 0 + 10 00040000 0 + 11 00040001 0 + 12 00040002 0 + 13 00050000 0 + 14 00050001 0 + 15 00050002 0 + 16 00060000 0 + 17 00060001 0 + 18 00060002 0 + 19 00070000 0 + 20 00070001 0 + 21 00070002 0 + 22 80020000 0 + 23 80020001 0 + 24 80020002 0 +} +Group.impFile=/TESTROOT/rect_v20.slvs +Group.impFileRel=rect_v20.slvs +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=80030000 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=80030001 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=80030002 +AddParam + +Param.h.v.=80030003 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=80030004 +AddParam + +Param.h.v.=80030005 +AddParam + +Param.h.v.=80030006 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=0 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030002 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030003 +Entity.type=3011 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030005 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030006 +Entity.type=3011 +Entity.construction=0 +Entity.point[0].v=80030005 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030008 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030009 +Entity.type=3011 +Entity.construction=0 +Entity.point[0].v=80030008 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000a +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000b +Entity.point[1].v=8003000c +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000b +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000c +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000d +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000e +Entity.point[1].v=8003000f +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000e +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000f +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030010 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030011 +Entity.point[1].v=80030012 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030011 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030012 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030013 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030014 +Entity.point[1].v=80030015 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030014 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030015 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030017 +Entity.type=3011 +Entity.construction=0 +Entity.point[0].v=80030018 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030018 +Entity.type=2011 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + diff --git a/test/group/link/rect_v20.slvs b/test/group/link/rect_v20.slvs new file mode 100644 index 0000000..3bebad4 --- /dev/null +++ b/test/group/link/rect_v20.slvs @@ -0,0 +1,510 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00060014 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00070010 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00070011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00070013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00070014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000007 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00060001 +Entity.point[1].v=00060002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00070000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00070001 +Entity.point[1].v=00070002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00070001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00070002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040001 +Constraint.ptB.v=00050002 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050001 +Constraint.ptB.v=00060002 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000003 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00060001 +Constraint.ptB.v=00070002 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000004 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00070001 +Constraint.ptB.v=00040002 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000005 +Constraint.type=81 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000006 +Constraint.type=80 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000007 +Constraint.type=81 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00060000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000008 +Constraint.type=80 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00070000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/group/link/test.cpp b/test/group/link/test.cpp new file mode 100644 index 0000000..5e9db1b --- /dev/null +++ b/test/group/link/test.cpp @@ -0,0 +1,17 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} diff --git a/test/group/translate_asy/normal.png b/test/group/translate_asy/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..c9b4e4ec831e041d2e339d3a4d1cc6de6951bde5 GIT binary patch literal 8508 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 literal 0 HcmV?d00001 diff --git a/test/group/translate_asy/normal.slvs b/test/group/translate_asy/normal.slvs new file mode 100644 index 0000000..b097270 --- /dev/null +++ b/test/group/translate_asy/normal.slvs @@ -0,0 +1,2104 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000003 +Group.type=5100 +Group.order=2 +Group.name=extrude +Group.opA.v=00000002 +Group.color=00646464 +Group.subtype=7000 +Group.skipFirst=0 +Group.meshCombine=2 +Group.predef.entityB.v=80020000 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ + 1 00040000 1002 + 2 00040001 1002 + 3 00040002 1002 + 4 00040000 1001 + 5 00040001 1001 + 6 00040002 1001 + 7 00040000 1004 + 8 00040001 1003 + 9 00040002 1003 + 10 00050000 1002 + 11 00050001 1002 + 12 00050002 1002 + 13 00050000 1001 + 14 00050001 1001 + 15 00050002 1001 + 16 00050000 1004 + 17 00050001 1003 + 18 00050002 1003 + 19 00060000 1002 + 20 00060001 1002 + 21 00060002 1002 + 22 00060000 1001 + 23 00060001 1001 + 24 00060002 1001 + 25 00060000 1004 + 26 00060001 1003 + 27 00060002 1003 + 28 00070000 1002 + 29 00070001 1002 + 30 00070002 1002 + 31 00070000 1001 + 32 00070001 1001 + 33 00070002 1001 + 34 00070000 1004 + 35 00070001 1003 + 36 00070002 1003 + 37 80020000 1002 + 38 80020000 1001 + 39 80020001 1002 + 40 80020002 1002 + 41 80020001 1001 + 42 80020002 1001 + 43 80020002 1003 + 44 00000000 1001 + 45 00000000 1002 +} +AddGroup + +Group.h.v=00000004 +Group.type=5201 +Group.order=3 +Group.name=translate +Group.opA.v=00000003 +Group.valA=2.00000000000000000000 +Group.color=00646464 +Group.subtype=7000 +Group.skipFirst=0 +Group.meshCombine=2 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ + 1 80030001 0 + 2 80030002 0 + 3 80030003 0 + 4 80030004 0 + 5 80030005 0 + 6 80030006 0 + 7 80030007 0 + 8 80030008 0 + 9 80030009 0 + 10 8003000a 0 + 11 8003000b 0 + 12 8003000c 0 + 13 8003000d 0 + 14 8003000e 0 + 15 8003000f 0 + 16 80030010 0 + 17 80030011 0 + 18 80030012 0 + 19 80030013 0 + 20 80030014 0 + 21 80030015 0 + 22 80030016 0 + 23 80030017 0 + 24 80030018 0 + 25 80030019 0 + 26 8003001a 0 + 27 8003001b 0 + 28 8003001c 0 + 29 8003001d 0 + 30 8003001e 0 + 31 8003001f 0 + 32 80030020 0 + 33 80030021 0 + 34 80030022 0 + 35 80030023 0 + 36 80030024 0 + 37 80030027 0 + 38 80030028 0 + 39 80030029 0 + 40 8003002a 0 + 41 8003002b 0 + 42 8003002c 0 + 43 8003002d 0 + 44 80030001 1 + 45 80030002 1 + 46 80030003 1 + 47 80030004 1 + 48 80030005 1 + 49 80030006 1 + 50 80030007 1 + 51 80030008 1 + 52 80030009 1 + 53 8003000a 1 + 54 8003000b 1 + 55 8003000c 1 + 56 8003000d 1 + 57 8003000e 1 + 58 8003000f 1 + 59 80030010 1 + 60 80030011 1 + 61 80030012 1 + 62 80030013 1 + 63 80030014 1 + 64 80030015 1 + 65 80030016 1 + 66 80030017 1 + 67 80030018 1 + 68 80030019 1 + 69 8003001a 1 + 70 8003001b 1 + 71 8003001c 1 + 72 8003001d 1 + 73 8003001e 1 + 74 8003001f 1 + 75 80030020 1 + 76 80030021 1 + 77 80030022 1 + 78 80030023 1 + 79 80030024 1 + 80 80030027 1 + 81 80030028 1 + 82 80030029 1 + 83 8003002a 1 + 84 8003002b 1 + 85 8003002c 1 + 86 8003002d 1 + 87 80030001 1000 + 88 80030002 1000 + 89 80030003 1000 + 90 80030004 1000 + 91 80030005 1000 + 92 80030006 1000 + 93 80030007 1000 + 94 80030008 1000 + 95 80030009 1000 + 96 8003000a 1000 + 97 8003000b 1000 + 98 8003000c 1000 + 99 8003000d 1000 + 100 8003000e 1000 + 101 8003000f 1000 + 102 80030010 1000 + 103 80030011 1000 + 104 80030012 1000 + 105 80030013 1000 + 106 80030014 1000 + 107 80030015 1000 + 108 80030016 1000 + 109 80030017 1000 + 110 80030018 1000 + 111 80030019 1000 + 112 8003001a 1000 + 113 8003001b 1000 + 114 8003001c 1000 + 115 8003001d 1000 + 116 8003001e 1000 + 117 8003001f 1000 + 118 80030020 1000 + 119 80030021 1000 + 120 80030022 1000 + 121 80030023 1000 + 122 80030024 1000 + 123 80030027 1000 + 124 80030028 1000 + 125 80030029 1000 + 126 8003002a 1000 + 127 8003002b 1000 + 128 8003002c 1000 + 129 8003002d 1000 +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00060014 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00070010 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00070011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00070013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00070014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=80030000 +AddParam + +Param.h.v.=80030001 +AddParam + +Param.h.v.=80030002 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=80040000 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=80040001 +Param.val=2.50000000000000000000 +AddParam + +Param.h.v.=80040002 +Param.val=-2.50000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000007 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00060001 +Entity.point[1].v=00060002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00070000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00070001 +Entity.point[1].v=00070002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00070001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00070002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=0 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030001 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.point[1].v=80030003 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030002 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030003 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030004 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030005 +Entity.point[1].v=80030006 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030005 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030006 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030007 +Entity.type=5001 +Entity.construction=0 +Entity.point[0].v=80030005 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actNormal.vx=-1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030008 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030005 +Entity.point[1].v=80030002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030009 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.point[1].v=80030003 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000a +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000b +Entity.point[1].v=8003000c +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000b +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000c +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000d +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000e +Entity.point[1].v=8003000f +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000e +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000f +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030010 +Entity.type=5001 +Entity.construction=0 +Entity.point[0].v=8003000e +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actNormal.vy=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030011 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000e +Entity.point[1].v=8003000b +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030012 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.point[1].v=8003000c +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030013 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030014 +Entity.point[1].v=80030015 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030014 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030015 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030016 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030017 +Entity.point[1].v=80030018 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030017 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030018 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030019 +Entity.type=5001 +Entity.construction=0 +Entity.point[0].v=80030017 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actNormal.vx=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003001a +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030017 +Entity.point[1].v=80030014 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003001b +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030018 +Entity.point[1].v=80030015 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003001c +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003001d +Entity.point[1].v=8003001e +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003001d +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003001e +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003001f +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030020 +Entity.point[1].v=80030021 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030020 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030021 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030022 +Entity.type=5001 +Entity.construction=0 +Entity.point[0].v=80030020 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actNormal.vy=-1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030023 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030020 +Entity.point[1].v=8003001d +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030024 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030021 +Entity.point[1].v=8003001e +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030027 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030028 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030028 +Entity.type=2010 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030029 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003002a +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003002a +Entity.type=2010 +Entity.construction=1 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003002b +Entity.type=11000 +Entity.construction=1 +Entity.point[0].v=8003002a +Entity.point[1].v=80030028 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003002c +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003002a +Entity.actPoint.z=10.00000000000000000000 +Entity.actNormal.vz=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003002d +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=80030028 +Entity.actNormal.vz=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040001 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040002 +Entity.point[1].v=80040003 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040002 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040003 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040004 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040005 +Entity.point[1].v=80040006 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040005 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040006 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040007 +Entity.type=5003 +Entity.construction=0 +Entity.point[0].v=80040005 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actNormal.vx=-1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040008 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040005 +Entity.point[1].v=80040002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040009 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040006 +Entity.point[1].v=80040003 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004000a +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004000b +Entity.point[1].v=8004000c +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004000b +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004000c +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004000d +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004000e +Entity.point[1].v=8004000f +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004000e +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004000f +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040010 +Entity.type=5003 +Entity.construction=0 +Entity.point[0].v=8004000e +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actNormal.vy=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040011 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004000e +Entity.point[1].v=8004000b +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040012 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004000f +Entity.point[1].v=8004000c +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040013 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040014 +Entity.point[1].v=80040015 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040014 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040015 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040016 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040017 +Entity.point[1].v=80040018 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040017 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040018 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040019 +Entity.type=5003 +Entity.construction=0 +Entity.point[0].v=80040017 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actNormal.vx=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004001a +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040017 +Entity.point[1].v=80040014 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004001b +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040018 +Entity.point[1].v=80040015 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004001c +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004001d +Entity.point[1].v=8004001e +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004001d +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004001e +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004001f +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040020 +Entity.point[1].v=80040021 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040020 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040021 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040022 +Entity.type=5003 +Entity.construction=0 +Entity.point[0].v=80040020 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actNormal.vy=-1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040023 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040020 +Entity.point[1].v=8004001d +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040024 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040021 +Entity.point[1].v=8004001e +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040025 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80040026 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040026 +Entity.type=2010 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040027 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80040028 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040028 +Entity.type=2010 +Entity.construction=1 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040029 +Entity.type=11000 +Entity.construction=1 +Entity.point[0].v=80040028 +Entity.point[1].v=80040026 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004002a +Entity.type=5003 +Entity.construction=0 +Entity.point[0].v=80040028 +Entity.actPoint.z=10.00000000000000000000 +Entity.actNormal.vz=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004002b +Entity.type=5003 +Entity.construction=0 +Entity.point[0].v=80040026 +Entity.actNormal.vz=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040057 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040058 +Entity.point[1].v=80040059 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040058 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040059 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004005a +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004005b +Entity.point[1].v=8004005c +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004005b +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004005c +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004005d +Entity.type=5003 +Entity.construction=0 +Entity.point[0].v=8004005b +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actNormal.vx=-1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004005e +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004005b +Entity.point[1].v=80040058 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004005f +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004005c +Entity.point[1].v=80040059 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040060 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040061 +Entity.point[1].v=80040062 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040061 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040062 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040063 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040064 +Entity.point[1].v=80040065 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040064 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040065 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040066 +Entity.type=5003 +Entity.construction=0 +Entity.point[0].v=80040064 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actNormal.vy=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040067 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040064 +Entity.point[1].v=80040061 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040068 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040065 +Entity.point[1].v=80040062 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040069 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004006a +Entity.point[1].v=8004006b +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004006a +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004006b +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004006c +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004006d +Entity.point[1].v=8004006e +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004006d +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004006e +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004006f +Entity.type=5003 +Entity.construction=0 +Entity.point[0].v=8004006d +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actNormal.vx=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040070 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004006d +Entity.point[1].v=8004006a +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040071 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004006e +Entity.point[1].v=8004006b +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040072 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040073 +Entity.point[1].v=80040074 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040073 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040074 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040075 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040076 +Entity.point[1].v=80040077 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040076 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040077 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040078 +Entity.type=5003 +Entity.construction=0 +Entity.point[0].v=80040076 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actNormal.vy=-1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040079 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040076 +Entity.point[1].v=80040073 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004007a +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040077 +Entity.point[1].v=80040074 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004007b +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8004007c +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004007c +Entity.type=2010 +Entity.construction=1 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004007d +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8004007e +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004007e +Entity.type=2010 +Entity.construction=1 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004007f +Entity.type=11000 +Entity.construction=1 +Entity.point[0].v=8004007e +Entity.point[1].v=8004007c +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040080 +Entity.type=5003 +Entity.construction=0 +Entity.point[0].v=8004007e +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actNormal.vz=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040081 +Entity.type=5003 +Entity.construction=0 +Entity.point[0].v=8004007c +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actNormal.vz=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040001 +Constraint.ptB.v=00050002 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050001 +Constraint.ptB.v=00060002 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000003 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00060001 +Constraint.ptB.v=00070002 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000004 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00070001 +Constraint.ptB.v=00040002 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000005 +Constraint.type=81 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000006 +Constraint.type=80 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000007 +Constraint.type=81 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00060000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000008 +Constraint.type=80 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00070000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Surface 00000001 00646464 8004002a 1 1 +SCtrl 0 0 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000001 0 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +TrimBy 00000004 0 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +TrimBy 00000007 0 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +TrimBy 0000000a 0 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +AddSurface +Surface 00000002 00646464 8004002b 1 1 +SCtrl 0 0 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000002 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +TrimBy 00000005 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +TrimBy 00000008 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 0000000b 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +AddSurface +Surface 00000003 00646464 80040010 1 1 +SCtrl 0 0 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000001 1 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +TrimBy 00000002 0 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +TrimBy 00000003 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +TrimBy 0000000c 0 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +AddSurface +Surface 00000004 00646464 80040019 1 1 +SCtrl 0 0 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000004 1 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +TrimBy 00000005 0 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 00000006 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +TrimBy 00000003 0 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +AddSurface +Surface 00000005 00646464 80040022 1 1 +SCtrl 0 0 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000007 1 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +TrimBy 00000008 0 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 00000009 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +TrimBy 00000006 0 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +AddSurface +Surface 00000006 00646464 80040007 1 1 +SCtrl 0 0 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000a 1 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +TrimBy 0000000b 0 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +TrimBy 0000000c 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +TrimBy 00000009 0 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +AddSurface +Surface 00000007 00646464 80040080 1 1 +SCtrl 0 0 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000d 0 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000010 0 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +TrimBy 00000013 0 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +TrimBy 00000016 0 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +AddSurface +Surface 00000008 00646464 80040081 1 1 +SCtrl 0 0 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000e 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +TrimBy 00000011 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +TrimBy 00000014 1 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +TrimBy 00000017 1 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +AddSurface +Surface 00000009 00646464 80040066 1 1 +SCtrl 0 0 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000d 1 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 0000000e 0 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +TrimBy 0000000f 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000018 0 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +AddSurface +Surface 0000000a 00646464 8004006f 1 1 +SCtrl 0 0 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000010 1 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000011 0 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +TrimBy 00000012 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +TrimBy 0000000f 0 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +AddSurface +Surface 0000000b 00646464 80040078 1 1 +SCtrl 0 0 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000013 1 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +TrimBy 00000014 0 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +TrimBy 00000015 1 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +TrimBy 00000012 0 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +AddSurface +Surface 0000000c 00646464 8004005d 1 1 +SCtrl 0 0 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000016 1 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +TrimBy 00000017 0 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +TrimBy 00000018 1 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000015 0 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +AddSurface +Curve 00000001 1 1 00000001 00000003 +CCtrl 0 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +CurvePt 1 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +AddCurve +Curve 00000002 1 1 00000002 00000003 +CCtrl 0 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +CurvePt 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000003 1 1 00000003 00000004 +CCtrl 0 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +CurvePt 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000004 1 1 00000001 00000004 +CCtrl 0 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +CurvePt 1 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +AddCurve +Curve 00000005 1 1 00000002 00000004 +CCtrl 0 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +CurvePt 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000006 1 1 00000004 00000005 +CCtrl 0 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +CurvePt 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000007 1 1 00000001 00000005 +CCtrl 0 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +AddCurve +Curve 00000008 1 1 00000002 00000005 +CCtrl 0 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000009 1 1 00000005 00000006 +CCtrl 0 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 0000000a 1 1 00000001 00000006 +CCtrl 0 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +AddCurve +Curve 0000000b 1 1 00000002 00000006 +CCtrl 0 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 0000000c 1 1 00000006 00000003 +CCtrl 0 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 0000000d 1 1 00000007 00000009 +CCtrl 0 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +CurvePt 1 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +AddCurve +Curve 0000000e 1 1 00000008 00000009 +CCtrl 0 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +CurvePt 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +AddCurve +Curve 0000000f 1 1 00000009 0000000a +CCtrl 0 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +CurvePt 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +AddCurve +Curve 00000010 1 1 00000007 0000000a +CCtrl 0 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +CurvePt 1 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +AddCurve +Curve 00000011 1 1 00000008 0000000a +CCtrl 0 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +CurvePt 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +AddCurve +Curve 00000012 1 1 0000000a 0000000b +CCtrl 0 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +CurvePt 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +AddCurve +Curve 00000013 1 1 00000007 0000000b +CCtrl 0 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +CurvePt 1 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +AddCurve +Curve 00000014 1 1 00000008 0000000b +CCtrl 0 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +CurvePt 1 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +AddCurve +Curve 00000015 1 1 0000000b 0000000c +CCtrl 0 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +CurvePt 1 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +AddCurve +Curve 00000016 1 1 00000007 0000000c +CCtrl 0 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +CurvePt 1 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +AddCurve +Curve 00000017 1 1 00000008 0000000c +CCtrl 0 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +CurvePt 1 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +AddCurve +Curve 00000018 1 1 0000000c 00000009 +CCtrl 0 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +CurvePt 1 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +AddCurve diff --git a/test/group/translate_asy/normal_v22.slvs b/test/group/translate_asy/normal_v22.slvs new file mode 100644 index 0000000..2f013bc --- /dev/null +++ b/test/group/translate_asy/normal_v22.slvs @@ -0,0 +1,2088 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000003 +Group.type=5100 +Group.order=2 +Group.name=extrude +Group.opA.v=00000002 +Group.color=00646464 +Group.subtype=7000 +Group.skipFirst=0 +Group.meshCombine=2 +Group.predef.entityB.v=80020000 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ + 1 00040000 1002 + 2 00040001 1002 + 3 00040002 1002 + 4 00040000 1001 + 5 00040001 1001 + 6 00040002 1001 + 7 00040000 1004 + 8 00040001 1003 + 9 00040002 1003 + 10 00050000 1002 + 11 00050001 1002 + 12 00050002 1002 + 13 00050000 1001 + 14 00050001 1001 + 15 00050002 1001 + 16 00050000 1004 + 17 00050001 1003 + 18 00050002 1003 + 19 00060000 1002 + 20 00060001 1002 + 21 00060002 1002 + 22 00060000 1001 + 23 00060001 1001 + 24 00060002 1001 + 25 00060000 1004 + 26 00060001 1003 + 27 00060002 1003 + 28 00070000 1002 + 29 00070001 1002 + 30 00070002 1002 + 31 00070000 1001 + 32 00070001 1001 + 33 00070002 1001 + 34 00070000 1004 + 35 00070001 1003 + 36 00070002 1003 + 37 80020000 1002 + 38 80020000 1001 + 39 80020001 1002 + 40 80020002 1002 + 41 80020001 1001 + 42 80020002 1001 + 43 80020002 1003 + 44 00000000 1001 + 45 00000000 1002 +} +AddGroup + +Group.h.v=00000004 +Group.type=5201 +Group.order=3 +Group.name=translate +Group.opA.v=00000003 +Group.valA=2.00000000000000000000 +Group.color=00646464 +Group.subtype=7000 +Group.skipFirst=0 +Group.meshCombine=2 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ + 1 80030001 0 + 2 80030002 0 + 3 80030003 0 + 4 80030004 0 + 5 80030005 0 + 6 80030006 0 + 7 80030007 0 + 8 80030008 0 + 9 80030009 0 + 10 8003000a 0 + 11 8003000b 0 + 12 8003000c 0 + 13 8003000d 0 + 14 8003000e 0 + 15 8003000f 0 + 16 80030010 0 + 17 80030011 0 + 18 80030012 0 + 19 80030013 0 + 20 80030014 0 + 21 80030015 0 + 22 80030016 0 + 23 80030017 0 + 24 80030018 0 + 25 80030019 0 + 26 8003001a 0 + 27 8003001b 0 + 28 8003001c 0 + 29 8003001d 0 + 30 8003001e 0 + 31 8003001f 0 + 32 80030020 0 + 33 80030021 0 + 34 80030022 0 + 35 80030023 0 + 36 80030024 0 + 37 80030027 0 + 38 80030028 0 + 39 80030029 0 + 40 8003002a 0 + 41 8003002b 0 + 42 8003002c 0 + 43 8003002d 0 + 44 80030001 1 + 45 80030002 1 + 46 80030003 1 + 47 80030004 1 + 48 80030005 1 + 49 80030006 1 + 50 80030007 1 + 51 80030008 1 + 52 80030009 1 + 53 8003000a 1 + 54 8003000b 1 + 55 8003000c 1 + 56 8003000d 1 + 57 8003000e 1 + 58 8003000f 1 + 59 80030010 1 + 60 80030011 1 + 61 80030012 1 + 62 80030013 1 + 63 80030014 1 + 64 80030015 1 + 65 80030016 1 + 66 80030017 1 + 67 80030018 1 + 68 80030019 1 + 69 8003001a 1 + 70 8003001b 1 + 71 8003001c 1 + 72 8003001d 1 + 73 8003001e 1 + 74 8003001f 1 + 75 80030020 1 + 76 80030021 1 + 77 80030022 1 + 78 80030023 1 + 79 80030024 1 + 80 80030027 1 + 81 80030028 1 + 82 80030029 1 + 83 8003002a 1 + 84 8003002b 1 + 85 8003002c 1 + 86 8003002d 1 + 87 80030001 1000 + 88 80030002 1000 + 89 80030003 1000 + 90 80030004 1000 + 91 80030005 1000 + 92 80030006 1000 + 93 80030007 1000 + 94 80030008 1000 + 95 80030009 1000 + 96 8003000a 1000 + 97 8003000b 1000 + 98 8003000c 1000 + 99 8003000d 1000 + 100 8003000e 1000 + 101 8003000f 1000 + 102 80030010 1000 + 103 80030011 1000 + 104 80030012 1000 + 105 80030013 1000 + 106 80030014 1000 + 107 80030015 1000 + 108 80030016 1000 + 109 80030017 1000 + 110 80030018 1000 + 111 80030019 1000 + 112 8003001a 1000 + 113 8003001b 1000 + 114 8003001c 1000 + 115 8003001d 1000 + 116 8003001e 1000 + 117 8003001f 1000 + 118 80030020 1000 + 119 80030021 1000 + 120 80030022 1000 + 121 80030023 1000 + 122 80030024 1000 + 123 80030027 1000 + 124 80030028 1000 + 125 80030029 1000 + 126 8003002a 1000 + 127 8003002b 1000 + 128 8003002c 1000 + 129 8003002d 1000 +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00050010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050011 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00050013 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00050014 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00060010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00060011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00060013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00060014 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00070010 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00070011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00070013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00070014 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=80030000 +AddParam + +Param.h.v.=80030001 +AddParam + +Param.h.v.=80030002 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=80040000 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=80040001 +Param.val=2.50000000000000000000 +AddParam + +Param.h.v.=80040002 +Param.val=-2.50000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000005 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000006 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Request.h.v=00000007 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00050001 +Entity.point[1].v=00050002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00050002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00060001 +Entity.point[1].v=00060002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00060002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00070000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00070001 +Entity.point[1].v=00070002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00070001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00070002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=0 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030001 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030002 +Entity.point[1].v=80030003 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030002 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030003 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030004 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030005 +Entity.point[1].v=80030006 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030005 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030006 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030007 +Entity.type=5001 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actNormal.vx=-1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030008 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030005 +Entity.point[1].v=80030002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030009 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030006 +Entity.point[1].v=80030003 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000a +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000b +Entity.point[1].v=8003000c +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000b +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000c +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000d +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000e +Entity.point[1].v=8003000f +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000e +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003000f +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030010 +Entity.type=5001 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actNormal.vy=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030011 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000e +Entity.point[1].v=8003000b +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030012 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003000f +Entity.point[1].v=8003000c +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030013 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030014 +Entity.point[1].v=80030015 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030014 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030015 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030016 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030017 +Entity.point[1].v=80030018 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030017 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030018 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030019 +Entity.type=5001 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actNormal.vx=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003001a +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030017 +Entity.point[1].v=80030014 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003001b +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030018 +Entity.point[1].v=80030015 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003001c +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003001d +Entity.point[1].v=8003001e +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003001d +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003001e +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003001f +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030020 +Entity.point[1].v=80030021 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030020 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030021 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030022 +Entity.type=5001 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actNormal.vy=-1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030023 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030020 +Entity.point[1].v=8003001d +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030024 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80030021 +Entity.point[1].v=8003001e +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030027 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80030028 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030028 +Entity.type=2010 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80030029 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8003002a +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003002a +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003002b +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8003002a +Entity.point[1].v=80030028 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003002c +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=8003002a +Entity.actPoint.z=10.00000000000000000000 +Entity.actNormal.vz=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8003002d +Entity.type=5000 +Entity.construction=0 +Entity.point[0].v=80030028 +Entity.actNormal.vz=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040001 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040002 +Entity.point[1].v=80040003 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040002 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040003 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040004 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040005 +Entity.point[1].v=80040006 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040005 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040006 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040007 +Entity.type=5003 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actNormal.vx=-1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040008 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040005 +Entity.point[1].v=80040002 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040009 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040006 +Entity.point[1].v=80040003 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004000a +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004000b +Entity.point[1].v=8004000c +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004000b +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004000c +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004000d +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004000e +Entity.point[1].v=8004000f +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004000e +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004000f +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040010 +Entity.type=5003 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actNormal.vy=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040011 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004000e +Entity.point[1].v=8004000b +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040012 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004000f +Entity.point[1].v=8004000c +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040013 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040014 +Entity.point[1].v=80040015 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040014 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040015 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040016 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040017 +Entity.point[1].v=80040018 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040017 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040018 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040019 +Entity.type=5003 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actNormal.vx=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004001a +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040017 +Entity.point[1].v=80040014 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004001b +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040018 +Entity.point[1].v=80040015 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004001c +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004001d +Entity.point[1].v=8004001e +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004001d +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004001e +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004001f +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040020 +Entity.point[1].v=80040021 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040020 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040021 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040022 +Entity.type=5003 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actNormal.vy=-1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040023 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040020 +Entity.point[1].v=8004001d +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040024 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040021 +Entity.point[1].v=8004001e +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040025 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80040026 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040026 +Entity.type=2010 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040027 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80040028 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040028 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.z=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040029 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040028 +Entity.point[1].v=80040026 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004002a +Entity.type=5003 +Entity.construction=0 +Entity.actPoint.z=10.00000000000000000000 +Entity.actNormal.vz=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004002b +Entity.type=5003 +Entity.construction=0 +Entity.actNormal.vz=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040057 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040058 +Entity.point[1].v=80040059 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040058 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040059 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004005a +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004005b +Entity.point[1].v=8004005c +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004005b +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004005c +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004005d +Entity.type=5003 +Entity.construction=0 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actNormal.vx=-1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004005e +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004005b +Entity.point[1].v=80040058 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004005f +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004005c +Entity.point[1].v=80040059 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040060 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040061 +Entity.point[1].v=80040062 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040061 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040062 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040063 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040064 +Entity.point[1].v=80040065 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040064 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040065 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040066 +Entity.type=5003 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actNormal.vy=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040067 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040064 +Entity.point[1].v=80040061 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040068 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040065 +Entity.point[1].v=80040062 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040069 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004006a +Entity.point[1].v=8004006b +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004006a +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004006b +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004006c +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004006d +Entity.point[1].v=8004006e +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004006d +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004006e +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004006f +Entity.type=5003 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actNormal.vx=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040070 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004006d +Entity.point[1].v=8004006a +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040071 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004006e +Entity.point[1].v=8004006b +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040072 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040073 +Entity.point[1].v=80040074 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040073 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040074 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040075 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040076 +Entity.point[1].v=80040077 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040076 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040077 +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040078 +Entity.type=5003 +Entity.construction=0 +Entity.actPoint.x=20.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actNormal.vy=-1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040079 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040076 +Entity.point[1].v=80040073 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004007a +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=80040077 +Entity.point[1].v=80040074 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004007b +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8004007c +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004007c +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004007d +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=8004007e +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004007e +Entity.type=2010 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=8004007f +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=8004007e +Entity.point[1].v=8004007c +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040080 +Entity.type=5003 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=5.00000000000000000000 +Entity.actNormal.vz=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80040081 +Entity.type=5003 +Entity.construction=0 +Entity.actPoint.x=10.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actPoint.z=-5.00000000000000000000 +Entity.actNormal.vz=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00040001 +Constraint.ptB.v=00050002 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000002 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00050001 +Constraint.ptB.v=00060002 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000003 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00060001 +Constraint.ptB.v=00070002 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000004 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.ptA.v=00070001 +Constraint.ptB.v=00040002 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000005 +Constraint.type=81 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00040000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000006 +Constraint.type=80 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00050000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000007 +Constraint.type=81 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00060000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Constraint.h.v=00000008 +Constraint.type=80 +Constraint.group.v=00000002 +Constraint.workplane.v=80020000 +Constraint.entityA.v=00070000 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + +Surface 00000001 00646464 8004002a 1 1 +SCtrl 0 0 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000010000000827 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 -5.00000000000000000000 -5.00000000010000000827 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 10.00000000010000000827 -5.00000000010000000827 10.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000001 0 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +TrimBy 00000004 0 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +TrimBy 00000007 0 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +TrimBy 0000000a 0 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +AddSurface +Surface 00000002 00646464 8004002b 1 1 +SCtrl 0 0 10.00000000010000000827 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 10.00000000010000000827 -5.00000000010000000827 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 -5.00000000000000000000 -5.00000000010000000827 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000002 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +TrimBy 00000005 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +TrimBy 00000008 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 0000000b 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +AddSurface +Surface 00000003 00646464 80040010 1 1 +SCtrl 0 0 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000001 1 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +TrimBy 00000002 0 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +TrimBy 00000003 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +TrimBy 0000000c 0 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +AddSurface +Surface 00000004 00646464 80040019 1 1 +SCtrl 0 0 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000004 1 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +TrimBy 00000005 0 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 00000006 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +TrimBy 00000003 0 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +AddSurface +Surface 00000005 00646464 80040022 1 1 +SCtrl 0 0 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000007 1 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +TrimBy 00000008 0 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +TrimBy 00000009 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +TrimBy 00000006 0 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +AddSurface +Surface 00000006 00646464 80040007 1 1 +SCtrl 0 0 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000a 1 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +TrimBy 0000000b 0 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +TrimBy 0000000c 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +TrimBy 00000009 0 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +AddSurface +Surface 00000007 00646464 80040080 1 1 +SCtrl 0 0 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 20.00000000010000178463 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 5.00000000000000000000 -0.00000000010000000827 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 20.00000000010000178463 -0.00000000010000000827 5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000d 0 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000010 0 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +TrimBy 00000013 0 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +TrimBy 00000016 0 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +AddSurface +Surface 00000008 00646464 80040081 1 1 +SCtrl 0 0 20.00000000010000178463 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 20.00000000010000178463 -0.00000000010000000827 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 5.00000000000000000000 -0.00000000010000000827 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000e 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +TrimBy 00000011 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +TrimBy 00000014 1 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +TrimBy 00000017 1 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +AddSurface +Surface 00000009 00646464 80040066 1 1 +SCtrl 0 0 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 0000000d 1 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 0000000e 0 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +TrimBy 0000000f 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000018 0 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +AddSurface +Surface 0000000a 00646464 8004006f 1 1 +SCtrl 0 0 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000010 1 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000011 0 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +TrimBy 00000012 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +TrimBy 0000000f 0 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +AddSurface +Surface 0000000b 00646464 80040078 1 1 +SCtrl 0 0 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000013 1 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +TrimBy 00000014 0 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +TrimBy 00000015 1 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +TrimBy 00000012 0 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +AddSurface +Surface 0000000c 00646464 8004005d 1 1 +SCtrl 0 0 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 0 1 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 0 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +SCtrl 1 1 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +TrimBy 00000016 1 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +TrimBy 00000017 0 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +TrimBy 00000018 1 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +TrimBy 00000015 0 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +AddSurface +Curve 00000001 1 1 00000001 00000003 +CCtrl 0 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +CurvePt 1 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +AddCurve +Curve 00000002 1 1 00000002 00000003 +CCtrl 0 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +CurvePt 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000003 1 1 00000003 00000004 +CCtrl 0 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +CurvePt 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000004 1 1 00000001 00000004 +CCtrl 0 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +CurvePt 1 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +AddCurve +Curve 00000005 1 1 00000002 00000004 +CCtrl 0 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +CurvePt 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000006 1 1 00000004 00000005 +CCtrl 0 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +CurvePt 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000007 1 1 00000001 00000005 +CCtrl 0 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +AddCurve +Curve 00000008 1 1 00000002 00000005 +CCtrl 0 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 -5.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 00000009 1 1 00000005 00000006 +CCtrl 0 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 0000000a 1 1 00000001 00000006 +CCtrl 0 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 10.00000000000000000000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +AddCurve +Curve 0000000b 1 1 00000002 00000006 +CCtrl 0 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 5.00000000000000000000 0.00000000000000000000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 0000000c 1 1 00000006 00000003 +CCtrl 0 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 10.00000000000000000000 +CurvePt 1 10.00000000000000000000 -5.00000000000000000000 0.00000000000000000000 +AddCurve +Curve 0000000d 1 1 00000007 00000009 +CCtrl 0 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +CurvePt 1 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +AddCurve +Curve 0000000e 1 1 00000008 00000009 +CCtrl 0 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +CurvePt 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +AddCurve +Curve 0000000f 1 1 00000009 0000000a +CCtrl 0 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +CurvePt 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +AddCurve +Curve 00000010 1 1 00000007 0000000a +CCtrl 0 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +CurvePt 1 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +AddCurve +Curve 00000011 1 1 00000008 0000000a +CCtrl 0 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +CurvePt 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +AddCurve +Curve 00000012 1 1 0000000a 0000000b +CCtrl 0 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +CurvePt 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +AddCurve +Curve 00000013 1 1 00000007 0000000b +CCtrl 0 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +CurvePt 1 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +AddCurve +Curve 00000014 1 1 00000008 0000000b +CCtrl 0 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 5.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +CurvePt 1 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +AddCurve +Curve 00000015 1 1 0000000b 0000000c +CCtrl 0 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +CurvePt 1 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +AddCurve +Curve 00000016 1 1 00000007 0000000c +CCtrl 0 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 20.00000000000000000000 10.00000000000000000000 5.00000000000000000000 +CurvePt 1 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +AddCurve +Curve 00000017 1 1 00000008 0000000c +CCtrl 0 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 20.00000000000000000000 10.00000000000000000000 -5.00000000000000000000 +CurvePt 1 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +AddCurve +Curve 00000018 1 1 0000000c 00000009 +CCtrl 0 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 Weight 1.00000000000000000000 +CCtrl 1 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 Weight 1.00000000000000000000 +CurvePt 1 20.00000000000000000000 0.00000000000000000000 5.00000000000000000000 +CurvePt 1 20.00000000000000000000 0.00000000000000000000 -5.00000000000000000000 +AddCurve diff --git a/test/group/translate_asy/test.cpp b/test/group/translate_asy/test.cpp new file mode 100644 index 0000000..979081d --- /dev/null +++ b/test/group/translate_asy/test.cpp @@ -0,0 +1,31 @@ +#include "solvespace.h" + +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER_ISO("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_inters) { + CHECK_LOAD("normal.slvs"); + + Group *g = SK.GetGroup(SS.GW.activeGroup); + g->GenerateDisplayItems(); + SMesh *m = &g->displayMesh; + + SEdgeList el = {}; + bool inters, leaks; + SKdNode::From(m)->MakeCertainEdgesInto(&el, + EdgeKind::SELF_INTER, /*coplanarIsInter=*/false, &inters, &leaks); + el.Clear(); + + // The assembly is supposed to interfere. + CHECK_TRUE(inters); +} diff --git a/test/group/translate_nd/normal.png b/test/group/translate_nd/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..4f5ee2c74ccb18e3990e448ba91446e4f0d2cfcc GIT binary patch literal 4983 zcmd5=dsx!<9{z%v<|S-d&AiaoN-HXxndH39Y|dk@i>8zYIF~0QGt0ZC*hN>CJnb~G zC^c)=6ctOfloT`Nlz6F*Xd#+0@j_~ah@isxfv!opX3u%{Jm-1fA1=S&=lglz&-=cg z->Gfix$5gI)&T(MyM61j0{}Qi`PQBdeNqva-Uh&Y2RE0my`nDl^E`GKKd4CVq#9FS z%>B)D=stGVakFSG)6ndOFZJpU)tu@vwd{kX5Zwlui!TND%mp~NvovDnP$4SA{1`JX zY^OJQMdd@(wJY?JAOP-Vwx2 zPQ!ri-r3XihZw)bQ>;yFH{4^R-poyeU{8B(KwP%&_#fSFLKOPzggMWk$C!fb?nZS~ znl;l}3^l;QhkDbYHlCi-HxJxhQFc&U4dNuuvz!HxzI&*A)!q^pvT?ZoHd7>H8W0)Tt@u6XT=w zJ+tP&KL8MI!NT7LP8J^B$h80tb6a>%zkG`)!)xwFt(@G-J(t4`0kT%ytcxpl8Nr+Q zQgUt$a#Y8pv~9d2%;R|De44BAFbsEzE~*w=4Q3 z$&v(lFh}S4`3!F@`AEP2a-pp7;Uil8l%D-DsJvH(`pmoq|MN@lcR|`r$Ohng z;1v3OSb|<=#{f$kAw^tOm|s!Qfc{AmY9e}p7VKTYs)~cATDI=2l3`XHzlo7s=8S$U z5arO3%~#Rwha9~So!2grqC*|JA_nGOr)3i=}&SQ1W>|4Oq*#;ae)t#?Eo`f&x}Oa3R)74NNNH4AS`HqJ$-8lkd`^SIo;&mgUnowQYx4$K~W^iSIX{t8=- zgEO$Yuz;h{yXDeC5alt^Bx=`G6HW*+4?k zya}Rs>J)I_AB$bTg>ETFM#h@Nny{p+2has(j-jREnzjEl)BCCqPEhL5O`ZmGhaYYy ziFhL?7`t)F>M*ufY(f*878uFqoX|z+} zm>$t6kzG*iIU-dk8iPH``5iKmJoG?Cg5!SVf}{k+=<^m1Sp+AZQB9>6{m_`Ey-3I%L<}m%$}>klEio;#2@u^8 zxOU}7zzJK)lJw%?5mIe+c(wFKCqxx&85v>;>2yrB$G`+vj)hBvA-xwSxN;b=u!$6j z;(JM3_{Q6=?W42x7QT$$Z8q|n-k7k^=B#>^+q10DDuD`xDQ%6e_VV_A#pmXG+ zp+x8sx?dp^#)fHLQR6;c`&uEU+{*~0%0(|CVeWya43>QB2g~2tfS2k!aXWLH2`bE zVkl*#u*`^>ZM_{3J#W1Ygn!ZL-(zjlS#j51u>rCDOu&PKnre9|JaK37avgAJ|Frhl zQ`Oo8TTh!eK9s0=7#vn*k29gc9xntC1RlNRFhXx`X*P0w zpJ13&>cNmm2P`t`8}dd4|LzwJ zEQU#}!!z_9t-N&Rv8DQwO}ItWa$JIYL~e-R|7~sf^*ne-J2owN&2w@n`3CWT9eqA+F#V{X+>7r+tuF}<7zZI&1b;q|Bq}qxuj$n3+w8X5x|HwMJ0z{!wH`D z0N{I7Qdvx2eiIP?C5uv?&ivgF;AG=nTBmAP+0v z)N7H}J0RVfP;M)<=xGxx2JSwrj(T3zI@IWMd}J7hXnPC|QFNo!vM3Y_PcccXnPakB z_DYhQ1R=Y2jeeO{-g)Nd-A;HqXDmw;A;;FIs3zw?^u^gwgjRtzt%#}O=J=y18SVm2Na(xKbIXcB77|XVWe38XGNG;euA!?HMH3vobHj`o z#5ljCdTr%ZElP67yvN*Y$fOeGlzzVy{U}4#f%G_&|cezUTv&3!({V2Ee_t)ATi=fHU o-FwPlX8X +#include +#include +#include +#include +#include + +#include "solvespace.h" + +#include "harness.h" + +#if defined(WIN32) +# include +#else +# include +#endif + +namespace SolveSpace { +namespace Platform { + // These are defined in guinone.cpp, and aren't exposed in solvespace.h. + extern std::vector fontFiles; +} +} + + +#ifdef TEST_BUILD_ON_WINDOWS +static const char *VALID_BUILD_PATH_SEPS = "/\\"; +static char BUILD_PATH_SEP = '\\'; +#else +static const char *VALID_BUILD_PATH_SEPS = "/"; +static char BUILD_PATH_SEP = '/'; +#endif + +static std::string BuildRoot() { + static std::string rootDir; + if(!rootDir.empty()) return rootDir; + + rootDir = __FILE__; + rootDir.erase(rootDir.find_last_of(VALID_BUILD_PATH_SEPS) + 1); + return rootDir; +} + +static Platform::Path HostRoot() { + static Platform::Path rootDir; + if(!rootDir.IsEmpty()) return rootDir; + + // No especially good way to do this, so let's assume somewhere up from + // the current directory there's our repository, with CMakeLists.txt, and + // pivot from there. + rootDir = Platform::Path::CurrentDirectory(); + + // We're never more than four levels deep. + for(size_t i = 0; i < 4; i++) { + FILE *f = OpenFile(rootDir.Join("CMakeLists.txt"), "r"); + if(f) { + fclose(f); + rootDir = rootDir.Join("test"); + return rootDir; + } + rootDir = rootDir.Parent(); + } + + ssassert(false, "Couldn't locate repository root"); +} + +enum class Color { + Red, + Green, + DarkGreen +}; + +static std::string Colorize(Color color, std::string input) { +#if !defined(WIN32) + if(isatty(fileno(stdout))) { + switch(color) { + case Color::Red: + return "\e[1;31m" + input + "\e[0m"; + case Color::Green: + return "\e[1;32m" + input + "\e[0m"; + case Color::DarkGreen: + return "\e[36m" + input + "\e[0m"; + } + } +#endif + return input; +} + +// Normalizes a savefile. Different platforms have slightly different floating-point +// behavior, so if we want to compare savefiles byte-by-byte, we need to do something +// to get rid of irrelevant differences in LSB. +static std::string PrepareSavefile(std::string data) { + // Round everything to 2**30 ~ 1e9 + const double precision = pow(2, 30); + + size_t lineBegin = 0; + while(lineBegin < data.length()) { + size_t nextLineBegin = data.find('\n', lineBegin); + if(nextLineBegin == std::string::npos) { + nextLineBegin = data.length(); + } else { + nextLineBegin++; + } + + size_t eqPos = data.find('=', lineBegin); + if(eqPos < nextLineBegin) { + std::string key = data.substr(lineBegin, eqPos - lineBegin), + value = data.substr(eqPos + 1, nextLineBegin - eqPos - 2); + + for(int i = 0; SolveSpaceUI::SAVED[i].type != 0; i++) { + if(SolveSpaceUI::SAVED[i].fmt != 'f') continue; + if(SolveSpaceUI::SAVED[i].desc != key) continue; + double f = strtod(value.c_str(), NULL); + f = round(f * precision) / precision; + std::string newValue = ssprintf("%.20f", f); + ssassert(value.size() == newValue.size(), "Expected no change in value length"); + std::copy(newValue.begin(), newValue.end(), + data.begin() + eqPos + 1); + } + + if(key == "Group.impFile") { + data.erase(lineBegin, nextLineBegin - lineBegin); + nextLineBegin = lineBegin; + } + } + + size_t spPos = data.find(' ', lineBegin); + if(spPos < nextLineBegin) { + std::string cmd = data.substr(lineBegin, spPos - lineBegin); + if(!cmd.empty()) { + if(cmd == "Surface" || cmd == "SCtrl" || cmd == "TrimBy" || + cmd == "Curve" || cmd == "CCtrl" || cmd == "CurvePt") { + data.erase(lineBegin, nextLineBegin - lineBegin); + nextLineBegin = lineBegin; + } + } + } + + lineBegin = nextLineBegin; + } + return data; +} + +bool Test::Helper::RecordCheck(bool success) { + checkCount++; + if(!success) failCount++; + return success; +} + +void Test::Helper::PrintFailure(const char *file, int line, std::string msg) { + std::string shortFile = file; + shortFile.erase(0, BuildRoot().size()); + fprintf(stderr, "test%c%s:%d: FAILED: %s\n", + BUILD_PATH_SEP, shortFile.c_str(), line, msg.c_str()); +} + +Platform::Path Test::Helper::GetAssetPath(std::string testFile, std::string assetName, + std::string mangle) { + if(!mangle.empty()) { + assetName.insert(assetName.rfind('.'), "." + mangle); + } + testFile.erase(0, BuildRoot().size()); + testFile.erase(testFile.find_last_of(VALID_BUILD_PATH_SEPS) + 1); + return HostRoot().Join(Platform::Path::FromPortable(testFile + assetName)); +} + +bool Test::Helper::CheckBool(const char *file, int line, const char *expr, bool value, + bool reference) { + if(!RecordCheck(value == reference)) { + std::string msg = ssprintf("(%s) = %s ≠ %s", expr, + value ? "true" : "false", + reference ? "true" : "false"); + PrintFailure(file, line, msg); + return false; + } else { + return true; + } +} + +bool Test::Helper::CheckEqualString(const char *file, int line, const char *valueExpr, + const std::string &value, const std::string &reference) { + if(!RecordCheck(value == reference)) { + std::string msg = ssprintf("(%s) = \"%s\" ≠ \"%s\"", valueExpr, + value.c_str(), reference.c_str()); + PrintFailure(file, line, msg); + return false; + } else { + return true; + } +} + +bool Test::Helper::CheckEqualEpsilon(const char *file, int line, const char *valueExpr, + double value, double reference) { + bool result = fabs(value - reference) < LENGTH_EPS; + if(!RecordCheck(result)) { + std::string msg = ssprintf("(%s) = %.4g ≉ %.4g", valueExpr, + value, reference); + PrintFailure(file, line, msg); + return false; + } else { + return true; + } +} + +bool Test::Helper::CheckLoad(const char *file, int line, const char *fixture) { + Platform::Path fixturePath = GetAssetPath(file, fixture); + + FILE *f = OpenFile(fixturePath, "rb"); + bool fixtureExists = (f != NULL); + if(f) fclose(f); + + bool result = fixtureExists && SS.LoadFromFile(fixturePath); + if(!RecordCheck(result)) { + PrintFailure(file, line, + ssprintf("loading file '%s'", fixturePath.raw.c_str())); + return false; + } else { + SS.AfterNewFile(); + SS.GW.offset = {}; + SS.GW.scale = 10.0; + return true; + } +} + +bool Test::Helper::CheckSave(const char *file, int line, const char *reference) { + Platform::Path refPath = GetAssetPath(file, reference), + outPath = GetAssetPath(file, reference, "out"); + if(!RecordCheck(SS.SaveToFile(outPath))) { + PrintFailure(file, line, + ssprintf("saving file '%s'", refPath.raw.c_str())); + return false; + } else { + std::string refData, outData; + ReadFile(refPath, &refData); + ReadFile(outPath, &outData); + if(!RecordCheck(PrepareSavefile(refData) == PrepareSavefile(outData))) { + PrintFailure(file, line, "savefile doesn't match reference"); + return false; + } + + RemoveFile(outPath); + return true; + } +} + +bool Test::Helper::CheckRender(const char *file, int line, const char *reference) { + // First, render to a framebuffer. + Camera camera = {}; + camera.pixelRatio = 1; + camera.gridFit = true; + camera.width = 600; + camera.height = 600; + camera.projUp = SS.GW.projUp; + camera.projRight = SS.GW.projRight; + camera.scale = SS.GW.scale; + + CairoPixmapRenderer pixmapCanvas; + pixmapCanvas.SetLighting(SS.GW.GetLighting()); + pixmapCanvas.SetCamera(camera); + pixmapCanvas.Init(); + + pixmapCanvas.StartFrame(); + SS.GW.Draw(&pixmapCanvas); + pixmapCanvas.FlushFrame(); + pixmapCanvas.FinishFrame(); + std::shared_ptr frame = pixmapCanvas.ReadFrame(); + + pixmapCanvas.Clear(); + + // Now, diff framebuffer against reference render. + Platform::Path refPath = GetAssetPath(file, reference), + outPath = GetAssetPath(file, reference, "out"), + diffPath = GetAssetPath(file, reference, "diff"); + + std::shared_ptr refPixmap = Pixmap::ReadPng(refPath, /*flip=*/true); + if(!RecordCheck(refPixmap && refPixmap->Equals(*frame))) { + frame->WritePng(outPath, /*flip=*/true); + + if(!refPixmap) { + PrintFailure(file, line, "reference render not present"); + return false; + } + + ssassert(refPixmap->format == frame->format, "Expected buffer formats to match"); + if(refPixmap->width != frame->width || + refPixmap->height != frame->height) { + PrintFailure(file, line, "render doesn't match reference; dimensions differ"); + } else { + std::shared_ptr diffPixmap = + Pixmap::Create(refPixmap->format, refPixmap->width, refPixmap->height); + + int diffPixelCount = 0; + for(size_t j = 0; j < refPixmap->height; j++) { + for(size_t i = 0; i < refPixmap->width; i++) { + if(!refPixmap->GetPixel(i, j).Equals(frame->GetPixel(i, j))) { + diffPixelCount++; + diffPixmap->SetPixel(i, j, RgbaColor::From(255, 0, 0, 255)); + } + } + } + + diffPixmap->WritePng(diffPath, /*flip=*/true); + std::string message = + ssprintf("render doesn't match reference; %d (%.2f%%) pixels differ", + diffPixelCount, + 100.0 * diffPixelCount / (refPixmap->width * refPixmap->height)); + PrintFailure(file, line, message); + } + return false; + } else { + RemoveFile(outPath); + RemoveFile(diffPath); + return true; + } +} + +bool Test::Helper::CheckRenderXY(const char *file, int line, const char *fixture) { + SS.GW.projRight = Vector::From(1, 0, 0); + SS.GW.projUp = Vector::From(0, 1, 0); + return CheckRender(file, line, fixture); +} + +bool Test::Helper::CheckRenderIso(const char *file, int line, const char *fixture) { + SS.GW.projRight = Vector::From(0.707, 0.000, -0.707); + SS.GW.projUp = Vector::From(-0.408, 0.816, -0.408); + return CheckRender(file, line, fixture); +} + +// Avoid global constructors; using a global static vector instead of a local one +// breaks MinGW for some obscure reason. +static std::vector *testCasesPtr; +int Test::Case::Register(Test::Case testCase) { + static std::vector testCases; + testCases.push_back(testCase); + testCasesPtr = &testCases; + return 0; +} + +int main(int argc, char **argv) { + std::vector args = Platform::InitCli(argc, argv); + + std::string filterPattern = ".*"; + unsigned int seed = std::random_device{}(); + if(args.size() == 1) { + } else if(args.size() == 2) { + filterPattern = args[1]; + } else if(args.size() == 3) { + filterPattern = args[1]; + seed = std::stoul(args[2]); + } else { + fprintf(stderr, "Usage: %s [test filter regex] [shuffle seed]\n", args[0].c_str()); + return 1; + } + + fprintf(stderr, "info: using test filter `%s' and seed %u\n", filterPattern.c_str(), seed); + + Platform::fontFiles.push_back(HostRoot().Join("Gentium-R.ttf")); + + std::mt19937 g(seed); + + // Wreck order dependencies between tests! + std::shuffle(testCasesPtr->begin(), testCasesPtr->end(), g); + + std::regex filter(filterPattern); + + auto testStartTime = std::chrono::steady_clock::now(); + size_t ranTally = 0, skippedTally = 0, checkTally = 0, failTally = 0; + for(Test::Case &testCase : *testCasesPtr) { + std::string testCaseName = testCase.fileName; + testCaseName.erase(0, BuildRoot().size()); + testCaseName.erase(testCaseName.find_last_of(VALID_BUILD_PATH_SEPS)); + testCaseName += BUILD_PATH_SEP + testCase.caseName; + + std::smatch filterMatch; + if(!std::regex_search(testCaseName, filterMatch, filter)) { + skippedTally += 1; + continue; + } + + SS.Init(); + SS.showToolbar = false; + SS.checkClosedContour = false; + + Test::Helper helper = {}; + testCase.fn(&helper); + + SK.Clear(); + SS.Clear(); + + ranTally += 1; + checkTally += helper.checkCount; + failTally += helper.failCount; + if(helper.checkCount == 0) { + fprintf(stderr, " %s test %s (empty)\n", + Colorize(Color::Red, "??").c_str(), + Colorize(Color::DarkGreen, testCaseName).c_str()); + } else if(helper.failCount > 0) { + fprintf(stderr, " %s test %s\n", + Colorize(Color::Red, "NG").c_str(), + Colorize(Color::DarkGreen, testCaseName).c_str()); + } else { + fprintf(stderr, " %s test %s\n", + Colorize(Color::Green, "OK").c_str(), + Colorize(Color::DarkGreen, testCaseName).c_str()); + } + } + + auto testEndTime = std::chrono::steady_clock::now(); + std::chrono::duration testTime = testEndTime - testStartTime; + + if(failTally > 0) { + fprintf(stderr, "Failure! %u checks failed\n", + (unsigned)failTally); + } else { + fprintf(stderr, "Success! %u test cases (%u skipped), %u checks, %.3fs\n", + (unsigned)ranTally, (unsigned)skippedTally, + (unsigned)checkTally, testTime.count()); + } + + // At last, try to reset all caches we or our dependencies have, to make SNR + // of memory checking tools like valgrind higher. + cairo_debug_reset_static_data(); + + return (failTally > 0); +} diff --git a/test/harness.h b/test/harness.h new file mode 100644 index 0000000..91b6d60 --- /dev/null +++ b/test/harness.h @@ -0,0 +1,78 @@ +//----------------------------------------------------------------------------- +// Our harness for running test cases, and reusable checks. +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- +#include +#include + +#include "platform/platform.h" + +// Hack... we should rename the ones in ui.h instead. +#undef CHECK_TRUE +#undef CHECK_FALSE + +namespace SolveSpace { +namespace Test { + +class Helper { +public: + size_t checkCount; + size_t failCount; + + bool RecordCheck(bool success); + void PrintFailure(const char *file, int line, std::string msg); + Platform::Path GetAssetPath(std::string testFile, std::string assetName, + std::string mangle = ""); + + bool CheckBool(const char *file, int line, const char *expr, + bool value, bool reference); + bool CheckEqualString(const char *file, int line, const char *valueExpr, + const std::string &value, const std::string &reference); + bool CheckEqualEpsilon(const char *file, int line, const char *valueExpr, + double value, double reference); + bool CheckLoad(const char *file, int line, const char *fixture); + bool CheckSave(const char *file, int line, const char *reference); + bool CheckRender(const char *file, int line, const char *fixture); + bool CheckRenderXY(const char *file, int line, const char *fixture); + bool CheckRenderIso(const char *file, int line, const char *fixture); +}; + +class Case { +public: + std::string fileName; + std::string caseName; + std::function fn; + + static int Register(Case testCase); +}; + +} +} + +using namespace SolveSpace; + +#define TEST_CASE(name) \ + static void Test_##name(Test::Helper *); \ + static Test::Case TestCase_##name = { __FILE__, #name, Test_##name }; \ + static int TestReg_##name = Test::Case::Register(TestCase_##name); \ + static void Test_##name(Test::Helper *helper) // { ... } + +#define CHECK_TRUE(cond) \ + do { if(!helper->CheckBool(__FILE__, __LINE__, #cond, cond, true)) return; } while(0) +#define CHECK_FALSE(cond) \ + do { if(!helper->CheckBool(__FILE__, __LINE__, #cond, cond, false)) return; } while(0) +#define CHECK_EQ_STR(value, reference) \ + do { if(!helper->CheckEqualString(__FILE__, __LINE__, \ + #value, value, reference)) return; } while(0) +#define CHECK_EQ_EPS(value, reference) \ + do { if(!helper->CheckEqualEpsilon(__FILE__, __LINE__, \ + #value, value, reference)) return; } while(0) +#define CHECK_LOAD(fixture) \ + do { if(!helper->CheckLoad(__FILE__, __LINE__, fixture)) return; } while(0) +#define CHECK_SAVE(fixture) \ + do { if(!helper->CheckSave(__FILE__, __LINE__, fixture)) return; } while(0) +#define CHECK_RENDER(reference) \ + do { if(!helper->CheckRenderXY(__FILE__, __LINE__, reference)) return; } while(0) +#define CHECK_RENDER_ISO(reference) \ + do { if(!helper->CheckRenderIso(__FILE__, __LINE__, reference)) return; } while(0) diff --git a/test/request/arc_of_circle/normal.png b/test/request/arc_of_circle/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..2839007cc95fc697f1a43d21aae85094b3dc6524 GIT binary patch literal 4560 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 U!C812`=tiBIC<>gY=`6i1zXKwMF0Q* literal 0 HcmV?d00001 diff --git a/test/request/arc_of_circle/normal.slvs b/test/request/arc_of_circle/normal.slvs new file mode 100644 index 0000000..aaa6b8f --- /dev/null +++ b/test/request/arc_of_circle/normal.slvs @@ -0,0 +1,306 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040016 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040017 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=500 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=14000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.point[2].v=00040003 +Entity.normal.v=00040020 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040003 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/arc_of_circle/normal_v20.slvs b/test/request/arc_of_circle/normal_v20.slvs new file mode 100644 index 0000000..78d983b --- /dev/null +++ b/test/request/arc_of_circle/normal_v20.slvs @@ -0,0 +1,306 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040016 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040017 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=500 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=14000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.point[2].v=00040003 +Entity.normal.v=00040020 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040003 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/arc_of_circle/normal_v22.slvs b/test/request/arc_of_circle/normal_v22.slvs new file mode 100644 index 0000000..6f92e0e --- /dev/null +++ b/test/request/arc_of_circle/normal_v22.slvs @@ -0,0 +1,306 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040016 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040017 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=500 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=14000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.point[2].v=00040003 +Entity.normal.v=00040020 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040003 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/arc_of_circle/test.cpp b/test/request/arc_of_circle/test.cpp new file mode 100644 index 0000000..5e9db1b --- /dev/null +++ b/test/request/arc_of_circle/test.cpp @@ -0,0 +1,17 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} diff --git a/test/request/circle/free_in_3d.png b/test/request/circle/free_in_3d.png new file mode 100644 index 0000000000000000000000000000000000000000..31e2499f9dd2402663bfd7a77611b323bf906d42 GIT binary patch literal 4796 zcmeHLX;4#H7QTsj?3;qIRaPTNLlA5m7ZifXrhNvO7Q0wEy8g@`&KAhz;&h^%p7R-38phicuzW(;>Cch8jcMu~ zJ2Eu;nbOTVR=<&VhF4pg-?rjuUW5WB3uZz7EFAu#$EUeq?)0cGUId=`tbAl~gO**8kgx<+YUSygPeGr3HZ=n(0k{a{77I*`J41V1~ zoOQcXnShfUq7j+{?6lckfSRo!YeeT2aJ^ozaG|EnrAFKC7d_Rd$}!2du}UCVkLXOg zpwG$Kb80tg;ziEmrww7azzZrInnls*OLX4}IKg;*LsD<6wR~4dzh5G>;+x+xyRc-L zt#msKc3@!O=)RF&JZo)kD8PtDqO?P6=JiGeCU z?-vuub~(FiUV+E@0wTO}hSfK>_&nzuU~F-0`ev14*dBwuStHs@K;~tTTV%2WnbcvE zaD)j|tPQDVsRL^yAQehStdolCo5xOu6AmLoG}O~tXhAi`r5zj!4({Vj^V=@bd|q;g`k zx_uiU+9361I+KXqzIX4l91U+BuGWeh?DY4B;IEN&?+cW7l1VDTkIddUc|9m>FvFPk zx&qup{MCPjd)Xl{>%Bu^gXrC9D(^Yku`J1@LkgoGkN`Q2DOk}_Y7$+|cd45m<}gJr zmJw~5em9a@*RHvs`XpXY2l$f{Aoa8NAjI04LO20?o2sV_K`p8b$dG>?O_FCo>e-bL z71<-m+ZBZ9vgx&|9LsPFwj_|*FZPMd^N3=YHU||ZmpV8D{%wr>8WWdsi+%DUdlD*T z`EJI+nfhUP>cW9sijrt6sm4~%P#k%f@w+UZ4_iZY)9Kw!3L@|*hPxSOk->Y)lM>Ed z%OPa^zM5gPDTHiwmm*I#P5a*JTWDK^_>NZE+e*;5`NUzJUBl~*L!_gx;B^ZZ5{FUV zKjo@=zy~qd1NMQa2qzLM$bm(7aV#~BA4mq5*^S;j0im>_dc;%N@|Y|(iErTJl#b3< zC8{hh_d_0Bj1XAWv-yx=7aj=!%RtE%9~5~Ro3vt?AiP^{-BJd$;rii{E6JIa=q7to zO;cEHx5Lq>r6=Ca`I|UCD7vN)o|}(MUs}n2!5L_C4iXvFii=JlBS;CU3TX?@ENUNG zn=QS^k+kWQOMz#Bd%Tz_If6e2N$8 zeo%r-ZfNDjsPTfiZ*m!tmu+OEzpuU%{mWpjz-;R^^i+&6Rx~6Xy`d4^bO$4^)aMpR z!-Z-7ric+~%Jpq|k3$LK>dpM^X2`{jxQnzvCJt_vmPhS5B}Kc(#H4PJ;* zeayra_3EzQotF|do+zAK!Rlr*gWCovrJ3U~5xx?!fH{6J)JosEZ7Gsw=JgxoZNL3! z4a||j=V?X8yN0_O>bqa{)??EHZak417`O-(xJUS*<+XoplN(;62sU-g?J0d7J|C@c zH&Z)8cbcgo^U?oW#l0h?6yP71eW&i;?s}~l;`T$Q${aNwT$y?C+13BJaDQYKBBg{r z(R%^i{C!C0Y3X&s?lz-}bJSBHo#MH5yWy+`n31S+&{7|(#lK*f5o>?J@P7nD(;{Nl z^ieH;gFmT4uN=^Qb5E)V^+=* zKyuQ1mmB(0?yqILO^b!uO8j}iG8OUh&zFF&oI^=QY4^||*|pk6AF=foe%t+E!8~R~ z`3hygP6(~~^Yj@!Uzcr0; z-vX1faWg-ersp7K#kXP&zDp}De+b?iT6r(~A%URh5{R;lf2&-=O-?J}PNpLj21vfb zvAx6^o{6g((R2&MZ<1=EIU)`%{o~w>Nm>Nmd!pBd)QI3I1!K^)?rz7jjpu&H%?Gxo zeHcMXM-wusB(S(dJ)i_4q!OjLv0*)1i75E8Y)Rtk3N&3=?N$RFHZyTG1bxS0Gwtc? zY1r(m>1$`(MCjCJPTcCcg+L^@Ig}XH5_7D2DptMg?xy%o^y3ZTB*igQD}Yy>SkQl;F!(T3>ah$8T~jJ zlX1dp^Ye<5NeeW5H~`{$HD42IO2P@DlEy1V3q^H99(k%XDXx>|6S|g7<{`VYT5+Jo zOR%2(DnGdK+kT5;3`%rIE1h179NOx~*u7$Bs)*A3-JZcUouNO|A}PZ;m-b(+@~G#~ z!dO)=+F7N(y|=_sgh%DH-pV0O@?g~dsE~b1ux(cD#uwwg@$n;5p28^aBOIRa$2X<~ z^DI;U(Ffz#CLeC#1vicw?|U3GC1vh9)%{%S*SA_A@#!<6Z z%pMO7X}aq=TatO&g?!rw?xUzTu6xY)H?{M2v4lh8H@Oo*jEZ6w0r>FEfNlX zUCR}Y>nMW4mT;1}u<;&sVboAbP{WNjVYZPKkW&vxi(uwnYps$b4!2+P>y{iZs~>+x z9HqPr%^vCJ5Z+pByb|KPyas7#9Iub{PI5CSAOl#Z zJ#2oJ+%c!SRLXbcjGn?K@s^VHRXrI8ChBMb&jdZw{g5GzwMg^A&NIR%|E8}rD4c01 p+?4uK`{3h}_eshAFE@Ecrl>JJYI@9*DM(ieY_ZvHU1mi}_!G8`>W2UT literal 0 HcmV?d00001 diff --git a/test/request/circle/free_in_3d.slvs b/test/request/circle/free_in_3d.slvs new file mode 100644 index 0000000..41287aa --- /dev/null +++ b/test/request/circle/free_in_3d.slvs @@ -0,0 +1,294 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00040021 +AddParam + +Param.h.v.=00040022 +AddParam + +Param.h.v.=00040023 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/circle/free_in_3d_dof.png b/test/request/circle/free_in_3d_dof.png new file mode 100644 index 0000000000000000000000000000000000000000..2b5dff0e5ea948effb9fce02f99a9f59e235c707 GIT binary patch literal 7195 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 LW`_Ct* literal 0 HcmV?d00001 diff --git a/test/request/circle/free_in_3d_v20.slvs b/test/request/circle/free_in_3d_v20.slvs new file mode 100644 index 0000000..6966f95 --- /dev/null +++ b/test/request/circle/free_in_3d_v20.slvs @@ -0,0 +1,292 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00040021 +AddParam + +Param.h.v.=00040022 +AddParam + +Param.h.v.=00040023 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/circle/free_in_3d_v22.slvs b/test/request/circle/free_in_3d_v22.slvs new file mode 100644 index 0000000..ff35698 --- /dev/null +++ b/test/request/circle/free_in_3d_v22.slvs @@ -0,0 +1,294 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00040021 +AddParam + +Param.h.v.=00040022 +AddParam + +Param.h.v.=00040023 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/circle/normal.png b/test/request/circle/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..31e2499f9dd2402663bfd7a77611b323bf906d42 GIT binary patch literal 4796 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 literal 0 HcmV?d00001 diff --git a/test/request/circle/normal.slvs b/test/request/circle/normal.slvs new file mode 100644 index 0000000..1ea104f --- /dev/null +++ b/test/request/circle/normal.slvs @@ -0,0 +1,283 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/circle/normal_dof.png b/test/request/circle/normal_dof.png new file mode 100644 index 0000000000000000000000000000000000000000..0e62eba3dbcd8923d76338faf33196ccbd7ecc55 GIT binary patch literal 4830 zcmeHLdo+|=8{ac#TobuWQZC(eL8i_TVNggHapX3P7@^zLK~iFdnMoWfDvESmPCAv$ zFfN12$p<5HPHVycswh;eDqX!u^btUBjA>-g6CzCXUT&RR3;o!R?Y`+fHBd4A9H z+dKXTM|(wiO?d!-;^s}Z+W|o0r@pe%@JQ9Jv7N~6m|a`8?)Rr&x4s8p=Y(@50Muq# z13<;n&_G#_1;8B?23V&-NFb{Uzs(P+0CXLQ2oNLpKQf9+$vB!xAe`@tmeh;5)dwwW zkB~oqJ}BKtvFN_hU7@Y5EwSt{v9PcxwM^)rUn-0EJ3rs$2!2D-64jrZe7hf)anXf! z0K}yI<(|Z=8(Z`N-tMyj>n60OM+Qg-(mx_Z4N>n)n;{z6I(6GCk=Z78ql(|)+X@H3 z)d&fQOZ5Hs7M~T&-rY?Sj9RiMo`yZ_~aPT+s=H~48tR+EpjaYf^E$do6p;t z4)|I&XI0Ay05sy|{wnI(I>X+TNUGZx81obia-sn7htHmCcr+(om|?z%3k_gwBU}!3 zDSzJ9baHKDvbTyB0kCoBCy@UfUO}yeM*$$Jf5EW_S$9nbXrz4h{JvX>{|pnD8fCCm zBv0*B`%`w1`nFff0_vH6(qjte|5kdmC7mrsj^FBdpVqur{MHOOZX&L4?6-SrKA8xl z!Y@Mf%nB7(VWv$mV!s}XC-6CSJ<_j46wdWEDxD%guq|n^;2iJhJE4WSkSkO$q{g8z z3pR^-bE`we+gLBU5yW)fgXmbo%6{xz({tXwaA^sM`i{-99Pr<4 zQXfjH+kSB0a+61quBV19z@3U@b8`D8=QZ2PxRUs?WMGOV#0;!he7xnzlYnQ+yaYK6 z`3|%EritXuxxE-@vZYd5q2Z;E_o=rh8|U#7AUZBX;=j9L;sfnTk)3ShoVmSS(k4{$t3XeNr=8ABrL0M1J4$Px%^Wezw;F1Xaf^Xv_~qa+--@_ z^MvGN4Z>5)+Z_WC$EI1faY;T$)f0joSUjyuJRDt3BL$*+v}x9>GOWF(SVu>Nh|CDb zAu0+}@zM-=w`JVhhKNNfl&%OcG>jXKal-;`aI9-#^GA2l zRC5%D5qN26-W5Cq;GsM3sgUxeYU;YB)L z84ddnm>FclQR57xT7dae4G1o?>HH#>n$xRNx!U0FH&xDRON0DdVYQM^+N2ZV7>HbR z8<{8+Up1}#ah8DkvGfEVj=D5g7v681Axv!$yZFnC%l1;8PrY-W#kE=|2wg~B6w!s%gYPbVCPM z%UZUy8B7o@y;bY-mtvJ^XaXgRA$!c?iGFq{Qo&~-D6(CN)u5r}kT1fz3NpgcXV3ro zdkH)fm^VOqDL<7xM`V%UM`ir!fCP5t8+9xzm)#wN3QxticDMA3irFI`WMdGbhd<2| z;d|qqd8nyWW$^E+2df|z&f0R|RZQp5(0dp4I>Eo@(a@2~p3$lvker-BYaR1}naiH% z5moSv96WPVU2XSI3__!`H)7c$7BOga5$S1@iU&AOGj0sL8k=9&1c|GnVK}`QQ{Z zr}IQEuAp($zi}(6?&h$hV+gl=gquzp>oF$O4Qfpq)EbqWJe790H?c8g1RT-|F$-redzFu(p23Lbcy?Wml$xYaH%YLT15PvYWl=LDe(3o>Z9tDVBbrZ zuAC|WGZTnJZ@9X8ed*9sl3@l)=WETG_mMIH%5?Zh;3lQyAY$I7@z^hH{Ts=1pKY-8 zs``JbF1`}E|G+S<_5Xq4{|N@Z0%NNP37_RMj96Z(o&d^km=8o%g2c2i^8>0~#YC8@fnrI&0 zQdbk0vMifB7p5kmV8ls`ys7wRN0F9O+C z5*c?=jh#CwdgJ*fBsJ>f96VE!z_^{Z3t^qrgCw!ypJX}|gKQ(K>vLI2_Ht`*k_2t< zcHs*&SWs(9h*|bDatRW;CY?dVrS&;DRlj6a$+#soB0vMN3DRr~^On;GrfX?o!B^bD zK^OW&2aM1=76}(Kq)Ke8N;}1V27t^ywtWpYsV#{GKGDaz^*<#0`E-P)B@`FuQ zWuPm=geCofx(Dzy8s_7f@o;SC@@F$RwiuwiY&r?7Qy}r8B0yE9g)s$v?-10&1^TQi zr_=r0Qna_FbF}^YghIok6CO$Dylh^x855RB=$t>A<2713{G+7Uo)A5}e;*n*$X6HG zX3<0snDYmi57rDm7*5pzC7g)a3}JK0E=>{cItuab-lPVC03Ws!2mKnmY~FB$&rkfq zen#f5A9={U?QyA}BhJ*~i*Mk@(|1>w9nR>Y_>NW@KJ!Q%?+M}fl+1b8 zbtc7~)y3Vtr3*p}_``2W+PG9>-%$aX5G_2{ThcwAYTBPS$*d^8&+b~5MA6B=6&F{_ zv4^ABWq7CABK;{PYu1HK^7&pRXk!>HES9^SK^cDhowouoJyYd-EDYyq+Cy07ZO^F` z?XZfAyuSEt=lib8?ntfCw!yqaDQLxl#jJk2$Idx>^MnY|@{)cWzo(bQeLqU}Aw}Vd z%${B)YY#C+k;CNPBS$=9iO3UYrKv6g6ITEGfB@Wyn;x8^*E@QSHeeP4bYh`hc}vcL z5~oSKvHb+2!HLbjwMY5*F7D%J-}kkJR*NX*N1m5%@s0-j)H?H=L{(~;PH{?H&a2)R z)o3N9BuDL62j;lvrez{Poa-&SF@sH=^Lm-_8MwqOGog85_gJFU(4gw*sWSyhugt4n zRUhtPOBAe literal 0 HcmV?d00001 diff --git a/test/request/circle/normal_v20.slvs b/test/request/circle/normal_v20.slvs new file mode 100644 index 0000000..cdd2810 --- /dev/null +++ b/test/request/circle/normal_v20.slvs @@ -0,0 +1,283 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/circle/normal_v22.slvs b/test/request/circle/normal_v22.slvs new file mode 100644 index 0000000..d245f28 --- /dev/null +++ b/test/request/circle/normal_v22.slvs @@ -0,0 +1,283 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040040 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=400 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=13000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.distance.v=00040040 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040040 +Entity.type=4000 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actDistance=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/circle/test.cpp b/test/request/circle/test.cpp new file mode 100644 index 0000000..9a5d536 --- /dev/null +++ b/test/request/circle/test.cpp @@ -0,0 +1,47 @@ +#include "solvespace.h" + +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(free_in_3d_roundtrip) { + CHECK_LOAD("free_in_3d.slvs"); + CHECK_RENDER("free_in_3d.png"); + CHECK_SAVE("free_in_3d.slvs"); +} + +TEST_CASE(free_in_3d_migrate_from_v20) { + CHECK_LOAD("free_in_3d_v20.slvs"); + CHECK_SAVE("free_in_3d.slvs"); +} + +TEST_CASE(free_in_3d_migrate_from_v22) { + CHECK_LOAD("free_in_3d_v22.slvs"); + CHECK_SAVE("free_in_3d.slvs"); +} + +TEST_CASE(normal_dof) { + CHECK_LOAD("normal.slvs"); + SS.GenerateAll(SolveSpaceUI::Generate::ALL, /*andFindFree=*/true); + CHECK_RENDER("normal_dof.png"); +} + +TEST_CASE(free_in_3d_dof) { + CHECK_LOAD("free_in_3d.slvs"); + SS.GenerateAll(SolveSpaceUI::Generate::ALL, /*andFindFree=*/true); + CHECK_RENDER_ISO("free_in_3d_dof.png"); +} diff --git a/test/request/cubic/normal.png b/test/request/cubic/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..e7eb94d81a78bb0ccc1e5238b2b7fe99fb678396 GIT binary patch literal 4949 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 Y1<6ABkAArs{2Kvy?(y4QwTqhlPY<7wBme*a literal 0 HcmV?d00001 diff --git a/test/request/datum_point/normal.slvs b/test/request/datum_point/normal.slvs new file mode 100644 index 0000000..867c76b --- /dev/null +++ b/test/request/datum_point/normal.slvs @@ -0,0 +1,252 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/datum_point/normal_v20.slvs b/test/request/datum_point/normal_v20.slvs new file mode 100644 index 0000000..afec571 --- /dev/null +++ b/test/request/datum_point/normal_v20.slvs @@ -0,0 +1,250 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/datum_point/normal_v22.slvs b/test/request/datum_point/normal_v22.slvs new file mode 100644 index 0000000..50f8877 --- /dev/null +++ b/test/request/datum_point/normal_v22.slvs @@ -0,0 +1,252 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=101 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/datum_point/test.cpp b/test/request/datum_point/test.cpp new file mode 100644 index 0000000..5e9db1b --- /dev/null +++ b/test/request/datum_point/test.cpp @@ -0,0 +1,17 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} diff --git a/test/request/image/drawing.png b/test/request/image/drawing.png new file mode 100644 index 0000000000000000000000000000000000000000..56bedb7523f0a12bb142429e073d77ef8b58f8ac GIT binary patch literal 8565 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 literal 0 HcmV?d00001 diff --git a/test/request/image/linked.png b/test/request/image/linked.png new file mode 100644 index 0000000000000000000000000000000000000000..8667e34140ad2c7704ef6f2ca9b11630c3c03765 GIT binary patch literal 4309 zcmeHLYgAKL7CyNoyg~p)5tWCP7K@;UY72u|ez$gevgg_yHfDn|5AmX!t ziYQP?1p+uFyd>n(DolAfj3qq85C;`VgrJZ>AVM$JHJ!{Pv!*{fYu1|n$hvpspt!){%7eqnz0td;BGljCG+c0fc;DUDfZZLY%s}j`at^|oLh*_dE(Du@i4jY9-sjP>;_}&>%_9)FP zGr+Gj7GxIxGZ?kX&`#J!*=}KW-JB<0A%?6-pTjH)QaQ%@xkM6l-m(g9a8y zlw}6n@!0Via7aehYDE-pv#a;hAV9HD3gcpJ{3AhnbA&k1kZLBD_ls{Af$A>_GYSCGK7bD_FTvZ={D0`h0p?;ncjf=y!!iXKJ49 zOoy&Sthx|9F)Xa~P{`?BEgGfSHiMe={|}^^=o<)!iq|cofDp?&0>ED@hoq&RAO=RB zU6I=rct==4EWZs@u;8t*{a5&nsD!UO3V5`lRS79Ri;q-j<=J;-A#eR!-xLvI-O#yu z_LxX26IF;y zxI+5u8>S@2rsHt*j^r4OAsjptDUB0}OvZ7nd*2BMWLwS#$X<1x+T*nZO01smwXESV zb&HsWo6OseZjZ~+GK6oHdsdm|6FDtgurYV8Q|cm?)^;zb+Xu|*RIZbBPYx6EE}+kg3-c^e^|Y)R zE`Il#h4NLbsSTzcEQ(nQ27h6a%K9{;I3(mMsmT+KM5}p2{n(gvmwocQU%`=3y06}R z-QSKQ3n`9E3ADP=@rhzW`>m0O6)2UiNp_{Y6`RPFG`c}r8% z>bgDz+@(GfmABb33+vLL#9|tYEk?vD=to(8!vF)~J~2?* z8eg!>KiFiYP{r>=H%7)I&G~b*80UYH428c|+I|@qXx*!UF2eFheU0H^bF&TJsC(7( z)6(ETtqpb;`0oBv_a90jhpyLAM_FvH`N)){NLm&<`;DvFb(%OSPP!}Vp)MizmGaj_WY@emp@IoKJ8%NMCy^xnz4=|9elxn$K7?oY>-S| zki`#%_lY&;Cw&6nY&G65mNgD@pYp}8_(OLjtcCR=jq@uKy449ywmLd5O?xG%#!ZmWhWMf2{(-4LtU0 zLuC)Pj1{j2;Fm(E^fgKQA_h2gi98X7&i|JQhjE-m{h>`;LD;jUp*2q&PSm)MZUl_< zUnAXbyGmx!w9<(HaX0K`s@x68?WxahC{tpYXZA70F_up-0B@tb|0tc`=y-Ahn=Dd; z6VW^utqpP87cKtIz0m{uM2&B&4mw%v4n)Cm$eQ2I?7u*aNX;^}%^eWKrW=z@wTxN3 zy_zu(UeWfGO&tq%Wa$*jxsZpB!a++Vli9N^R>Vk-(01VGxG#g{Yk%-G3 zr>EqFdMW5}f!GRv#zsx#NHbv!W>{ag0s^;YTv=O4|K@Y`nd_Z!-xK8&^x^;+nW>l1 zsB^m}X^f;Z=GYNy8t8?CfOoaYi>|r3g9qmv6Y^u*u9=#ewj|EXiJm@<6cvrs+KgLn z1B_E8SnD2Sbicf6ZdKVN^^UI(1{mMBfkdSD(=7nSdC311edR&%lh2m{fx)@e#GW(O z;8t&L3VrOyf=B59t}Fb6*ne}HI+Ivl0fj%WMd=)FM{En6((P6G>FYmxm+)%-J}>VO uBC`x-(a!yLbGl-CQ<$9;-yQTWpiIW3+YHup{1tg!0o-n{>Fl)Q%)N7F?%c5O7P;Tt_EFcj4F5pfX;=zzB@imEgI_!j}r## zp6NJhfgG_y9PT3D%dfuCF@JAe#W%R@6ZT<*eM3O?N$q7qzVRIL&-oN8>q7vXNXZTW zVBAzR!A5@!3}k880q`68SAMSZg{L~_Dg}06J6OM8y)F4rp_u&AhzrX z$7K~D>*y`dG$$ggJ_yh+DBYx-3cg`5I!+7t-&bA9($sO*(Ra)bIWabLW;MCPwb)%b znaIE}mLUu{>b;L+NnG#q@w?N=I7^~y-w}7-S1?o?^vE)p26fF6;sg|! z;6%t5ABg>o+F!a7Vi(3pbQYF+uq(|+iDk8)c3;P@1;EAW_S;zY1_p>3pFPz6l|BC^ zWw~4U3_JqVtG~oB#Gc)9z`f?bf@8<38#Ym|gE(h^13?C)to=z!w_``s5_6RJ7F$L| z&X|LgS#+Q`M4^cSjdub4oC-EfxJC;kSgtS&w3iQ`FI0N7GC7qXImp1-ozqte(JnA~ z7_G{+7nhItLGhTlymftENmcL>Oa)f>cNoZx%tYu+vhgr(jJhrP3)5+v>MVnzJze@P z83>&hr3)H%{mB}(WOzZ;ZLEyX2ITtFX!)$SyQxBc7n|0Z94U`RYr3Bn%|2@7RIU45 z!@x0@(~O^fK5gaGWt(cOqS>$65J2}eXgGVFYH#Pa&~IAPa30;#w%JRiWKqa zLB`l0MKj07eQHecCjaDlMd5oC-M9r#m8TfXY#|O}XA7(9%W?P`*iM> zvv7{5eTVwE#AeEkJxQSHO&HMx9S=8@JY6$^@aQl`L&d21X0H3R8zVC+AgE2%U{UZ` zHT?06vhM6#AKbgGYOuYaOurwu9%MG@@9&=7w3FOgXQ|$>Bmn!hDnDPxi5ZMa+&ML{ zC4Xhvo5LryJj#a~%-*r)lhbgaZQ%$d&AiU~4;&b^u4rk%aaOIQ`=m%m3FFF}hIDuvvJyttm{#N0X4?YHHaSIomjOul z5{KOyOTq*)`qCiN6aIEcD=P+w#Mk2;RdG^OkjXv2vWX))rEq| zIX2IwUbe5ME*OXG)CA2=ewaY{d0J~h6pUK{)w$~Iu1o$k0Anj=87Ac5HL5)t?yB2Q z<5OW^$cbll$Ivz1QiV&iF2?i~j@fF*Cmv$%*!+YD$U%-Vn12mztIW~|MOV1nwtiv* zet-u^oHx=@?GIOjW=lHi-r{IYM>Q{w%rApvSMLiAeGbF8b?6x)9W{rVWO7Va)t%_% zFaZo{ zF3s*_@9h#1pULMt@>>+o8V_LqO|Y%!*A&zAmX{UMSn=Z(8Z+r>LKC&bV-uw-ET#0 z1i=pJR}?jGy<|y+q~mm@L|7{Hn0q8j7O}!+{3x;BHDccQG0A{5TKvR_{Bannreai{ z=RHS~J{m71nk17<`UXYBo}_ww3tpOZHLXZWkn76i&t~plr6o!Pttrn26PZ1+yg2B~ zi4JQY42rIer#MHZPcl3ffJd1F9sT&$hj?+r3w;w!kZKm;y=pYQI)TqTT$fABBQp)oqVe|HnQ2(k}z4w}G2`H!6l10sWl-dv~FHD|cct{{u|mo}2&x literal 0 HcmV?d00001 diff --git a/test/request/image/normal.slvs b/test/request/image/normal.slvs new file mode 100644 index 0000000..dab1385 --- /dev/null +++ b/test/request/image/normal.slvs @@ -0,0 +1,319 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +AddParam + +Param.h.v.=00040011 +Param.val=10.00000000000000000000 +AddParam + +Param.h.v.=00040013 +AddParam + +Param.h.v.=00040014 +AddParam + +Param.h.v.=00040016 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=00040017 +AddParam + +Param.h.v.=00040019 +Param.val=15.00000000000000000000 +AddParam + +Param.h.v.=0004001a +Param.val=10.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=700 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +Request.file=drawing.png +Request.aspectRatio=1.50000000000000000000 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=16000 +Entity.construction=0 +Entity.file=drawing.png +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.point[2].v=00040003 +Entity.point[3].v=00040004 +Entity.normal.v=00040020 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040003 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=15.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040004 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=15.00000000000000000000 +Entity.actPoint.y=10.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/image/test.cpp b/test/request/image/test.cpp new file mode 100644 index 0000000..a9507be --- /dev/null +++ b/test/request/image/test.cpp @@ -0,0 +1,13 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(linked_roundtrip) { + CHECK_LOAD("linked.slvs"); + CHECK_RENDER("linked.png"); + CHECK_SAVE("linked.slvs"); +} diff --git a/test/request/line_segment/normal.png b/test/request/line_segment/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..a7b1c341a730deb665021f7bc262b0c9ddd0e48d GIT binary patch literal 4246 zcmeHLc~FyA0)K>?$RVtNngZ6!+A5NAugal9ks5Rr3E_@%%N3LW8jggex>^d0ju06m zV^J&+2q*y>Ncd0(umqw4Ndc2^XoExo%7zFS*+j)Dot4s=?H@biADK6k@6CMg{oe0- z-`T(K_te!{rvm`c_1f!x2mlyH`P0%+{KJYQKLWrY*voy_;k3&W{38j2XyenvcXp-N z1s{T(FLsqPs^5o)WWTJk=$?}Ro6<{z;ljs7#{tBxC za{( z<2JCQRf!qmtP6piwbjldyc1&7ta`oPnbH2zr;K6a4ZpWIVv-%x-vU`vCO*An6LIB2 zS6fX$G%)(mZXqWwouCPtv=%3DO)y`{0dx)Z4i+S>Y)9k@0Woc_$4>%N?;iY~CFs>3 z?yDjg0bu)O>xUrD4GV5c1vTHkUw#HDqE{cFeo}jP7SqQVVJkc;Vp3e7Opo}$F4%GD zx$g7jGaOX_(&!@`N}7L!V~OfN!ttlT;Tq7bgP5f0yUesVOk|s|%y6GuE}AUcYIh@f zuqDyAK@yzaT9TDAIpkB=QY5hg!VabI*=q1@k+X!gGLJsj%5guLrOVS&lOI)k-uQ@Z zYqtM)+pia2q-h47(>4Z3T72x^s@@xj(zD{qYlX)R8qXI9%TyZw@@mQA4K3rT;o4SC1#e8*V$4r&T}KofVt^*HD?8Q*1N|t+r#4H)M)Oi( z*14^XW+e68(a_Nh*>kzoo~!D-4K;v8iZKrAJmlL@XqzSVl@At%E<7k!CQyFpf^b# zzuRG-#8)q8Y+*t7IfiK5CK%kbeY>D{uve$po^PdG7+odyZ4o-^TAU9pV>yyw#00i#m8S(aIMu7N`LTwHUq2HWEcn ziw9DU0pi9>Z?hJQuoObKimTVJFsuQYS5Lo;{-Z(ttHOi{#a6`*maBlg@VAsECXv%s zImZ|Y@7u)hBsMl=kb68}iMD=LFjJutz`nbf{L5Cgk7Q7y_y0kL6eh&LCjvm`OJS8? zhpT`yj7SCj5PO$!P(hjhD$wAGc*jt)iH8h0z#d)n=4ETT7*55<@303@!8=7E^)4h1 z`FrzpmoRQ=@25J}q<7i_kjd{{>4SF(he+&f|2F_S``ubhrkfqW9Sze!_th2PD=V3F7r>5s z1XUC}+^onkZjO2AIth^!nhw)M_azt5dtI|o-*>=>&vVT&zC9l3z6H<1`!He>do{+l zWn-*OnLj|5v7q9(k>t;MwgS`6e2Fe%v{cRCEVE53LCTqef$p#?fjK62r#^#0R7(mr z139RC7{k=7SZ%Hff?BVU>`Uew4bcFX024n(Re>RRu^_??OCgu90l8jS%D9gH$0^H|EiDUU-J(hx)4UJH+4%_)4ScuLb<6^L+rsKU@6KpUz* z!T5d(2mdsBI~ZcD-Hn04r!^9LCdCCHi%zI>#o7-EM%VK#y$QUZ=bx8u;w<#j4-v-F zw>7oo0h}m^MCTD2rUi4U=KQ%pKc6k8XI*KKMcXQBeluPrbIVQC&KIwqXaZmR<5*^1 znRCw%yn~yxuv=fZYo(!k@S_OVXS4XtOlhs`X!K+}Yu;kMJMbSRXBiDMvS`YD`^?x0 zvy8Y&mrw$8QkKx48Oi5V-;$jq)YUs_4tX{7AjEUeX~Ox$nRJ0|teG^;ETh{qKY}C5 zNM=R3&Ri13aCp|WlBg_NdPF{r&^Y?E13y%-G+!PE7xy^L~0-Mjdb5P{^B57halEh?`WP#H*lGqx-sbF^MfwfnhOIog} z3>miRQf28;;m+q6oM%rirIw4nY|v7P#^nX(aOTDw22vM1UXIm=jL0v9aA$uCpvi0o z>?R(BWr@X8g6SFR-D!Qd{I*!>bwSbX(X~@kkG6;CwU{9E+AZ4C8?>5|Q5-<@8E1OD z+`N}%!yPo*;YlsoKIwO~YmQrTQhpa7_KT=pao2q<;_3#D%QnNiV+$L^V94gpC5`u6 i6IfA{|1T%KR_z%1g3@)6SE2ac0K7c*yI1eVX8#AbP?XXD literal 0 HcmV?d00001 diff --git a/test/request/line_segment/normal.slvs b/test/request/line_segment/normal.slvs new file mode 100644 index 0000000..4914fef --- /dev/null +++ b/test/request/line_segment/normal.slvs @@ -0,0 +1,278 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/line_segment/normal_v20.slvs b/test/request/line_segment/normal_v20.slvs new file mode 100644 index 0000000..fb26b9f --- /dev/null +++ b/test/request/line_segment/normal_v20.slvs @@ -0,0 +1,278 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/line_segment/normal_v22.slvs b/test/request/line_segment/normal_v22.slvs new file mode 100644 index 0000000..167a2c2 --- /dev/null +++ b/test/request/line_segment/normal_v22.slvs @@ -0,0 +1,278 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/line_segment/test.cpp b/test/request/line_segment/test.cpp new file mode 100644 index 0000000..5e9db1b --- /dev/null +++ b/test/request/line_segment/test.cpp @@ -0,0 +1,17 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} diff --git a/test/request/ttf_text/kerning.png b/test/request/ttf_text/kerning.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6eab03574b83cf80bdde49751ae18e19dc4e7a GIT binary patch literal 6122 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 B@znqT literal 0 HcmV?d00001 diff --git a/test/request/ttf_text/normal.slvs b/test/request/ttf_text/normal.slvs new file mode 100644 index 0000000..229c63b --- /dev/null +++ b/test/request/ttf_text/normal.slvs @@ -0,0 +1,329 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040016 +Param.val=23.92131768269594616072 +AddParam + +Param.h.v.=00040017 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040019 +Param.val=23.92131768269594616072 +AddParam + +Param.h.v.=0004001a +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=600 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +Request.str=Text +Request.font=Gentium-R.ttf +Request.aspectRatio=2.89213176826959461607 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=15000 +Entity.construction=0 +Entity.str=Text +Entity.font=Gentium-R.ttf +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.point[2].v=00040003 +Entity.point[3].v=00040004 +Entity.normal.v=00040020 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040003 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=23.92131768269594616072 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040004 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=23.92131768269594616072 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/ttf_text/normal_v20.slvs b/test/request/ttf_text/normal_v20.slvs new file mode 100644 index 0000000..65bc54a --- /dev/null +++ b/test/request/ttf_text/normal_v20.slvs @@ -0,0 +1,290 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=-5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=600 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +Request.str=Text +Request.font=Gentium-R.ttf +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=15000 +Entity.construction=0 +Entity.str=Text +Entity.font=GOST.ttf +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.normal.v=00040020 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/ttf_text/normal_v22.slvs b/test/request/ttf_text/normal_v22.slvs new file mode 100644 index 0000000..3022dcf --- /dev/null +++ b/test/request/ttf_text/normal_v22.slvs @@ -0,0 +1,292 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=-5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=600 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +Request.str=Text +Request.font=Gentium-R.ttf +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=15000 +Entity.construction=0 +Entity.str=Text +Entity.font=Gentium-R.ttf +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.normal.v=00040020 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=-5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3001 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.workplane.v=80020000 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/ttf_text/test.cpp b/test/request/ttf_text/test.cpp new file mode 100644 index 0000000..2937f79 --- /dev/null +++ b/test/request/ttf_text/test.cpp @@ -0,0 +1,23 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(kerning_roundtrip) { + CHECK_LOAD("kerning.slvs"); + CHECK_RENDER("kerning.png"); + CHECK_SAVE("kerning.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} diff --git a/test/request/workplane/normal.png b/test/request/workplane/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..30862b3ff1cd95fb5a817251fb9413f14d037231 GIT binary patch literal 5612 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 literal 0 HcmV?d00001 diff --git a/test/request/workplane/normal.slvs b/test/request/workplane/normal.slvs new file mode 100644 index 0000000..de433f9 --- /dev/null +++ b/test/request/workplane/normal.slvs @@ -0,0 +1,293 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +AddParam + +Param.h.v.=00040011 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040020 +Param.val=0.88047623921714945894 +AddParam + +Param.h.v.=00040021 +Param.val=-0.27984814233312144127 +AddParam + +Param.h.v.=00040022 +Param.val=-0.36470519963100095362 +AddParam + +Param.h.v.=00040023 +Param.val=-0.11591689595929521861 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=100 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.actNormal.w=0.88047623921714945894 +Entity.actNormal.vx=-0.27984814233312144127 +Entity.actNormal.vy=-0.36470519963100095362 +Entity.actNormal.vz=-0.11591689595929521861 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=1 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.ptA.v=00010001 +Constraint.ptB.v=00040001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/request/workplane/normal_v20.slvs b/test/request/workplane/normal_v20.slvs new file mode 100644 index 0000000..f239329 --- /dev/null +++ b/test/request/workplane/normal_v20.slvs @@ -0,0 +1,291 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +AddParam + +Param.h.v.=00040011 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040020 +Param.val=0.88047623921714946000 +AddParam + +Param.h.v.=00040021 +Param.val=-0.27984814233312144000 +AddParam + +Param.h.v.=00040022 +Param.val=-0.36470519963100095000 +AddParam + +Param.h.v.=00040023 +Param.val=-0.11591689595929522000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=100 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.actNormal.w=0.88047623921714946000 +Entity.actNormal.vx=-0.27984814233312144000 +Entity.actNormal.vy=-0.36470519963100095000 +Entity.actNormal.vz=-0.11591689595929522000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.ptA.v=00010001 +Constraint.ptB.v=00040001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/request/workplane/normal_v22.slvs b/test/request/workplane/normal_v22.slvs new file mode 100644 index 0000000..d79e899 --- /dev/null +++ b/test/request/workplane/normal_v22.slvs @@ -0,0 +1,293 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +AddParam + +Param.h.v.=00040011 +AddParam + +Param.h.v.=00040012 +AddParam + +Param.h.v.=00040020 +Param.val=0.88047623921714945894 +AddParam + +Param.h.v.=00040021 +Param.val=-0.27984814233312144127 +AddParam + +Param.h.v.=00040022 +Param.val=-0.36470519963100095362 +AddParam + +Param.h.v.=00040023 +Param.val=-0.11591689595929521861 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=100 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.normal.v=00040020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.actNormal.w=0.88047623921714945894 +Entity.actNormal.vx=-0.27984814233312144127 +Entity.actNormal.vy=-0.36470519963100095362 +Entity.actNormal.vz=-0.11591689595929521861 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Constraint.h.v=00000001 +Constraint.type=20 +Constraint.group.v=00000002 +Constraint.ptA.v=00010001 +Constraint.ptB.v=00040001 +Constraint.other=0 +Constraint.other2=0 +Constraint.reference=0 +AddConstraint + diff --git a/test/request/workplane/test.cpp b/test/request/workplane/test.cpp new file mode 100644 index 0000000..5e9db1b --- /dev/null +++ b/test/request/workplane/test.cpp @@ -0,0 +1,17 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v20) { + CHECK_LOAD("normal_v20.slvs"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(normal_migrate_from_v22) { + CHECK_LOAD("normal_v22.slvs"); + CHECK_SAVE("normal.slvs"); +} diff --git a/wishlist.txt b/wishlist.txt new file mode 100644 index 0000000..f9db34a --- /dev/null +++ b/wishlist.txt @@ -0,0 +1,19 @@ +O(n*log(n)) assembly of edges into contours +fix anti-aliased edge bug with filled contours +crude DXF, HPGL import +a request to import a plane thing +make export assemble only contours in same group +rotation of model view works about z of first point under cursor +a way to kill a slow operation + +----- +rounding, as a special group +associative entities from solid model, as a special group +better level of detail +some kind of import +faster triangulation +loop detection +IGES export +incremental regen of entities + + -- 2.39.5

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

sXIBX-lLa%ZNE>fQR7t?h1ze9{hw6uRG! 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 + + com.solvespace.SolveSpace + + SolveSpace +

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%0DvGE}uA3@Pvj=FV7P3SgFh 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